diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml
index 6527894333..1ef5db9fa5 100644
--- a/.idea/codeStyles/Project.xml
+++ b/.idea/codeStyles/Project.xml
@@ -9,8 +9,6 @@
-
-
@@ -200,4 +198,4 @@
-
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index ce1b62532a..5294908fc5 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -15,6 +15,9 @@
+
+
+
@@ -85,8 +88,6 @@
-
-
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index f3691ec571..ea77304740 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -11,7 +11,8 @@ plugins {
dependencies {
implementation(files("libs/walletconnect-1.5.6.aar"))
- implementation(project(":domain"))
+ implementation(project(":domain:legacy"))
+ implementation(project(":domain:models"))
implementation(project(":common"))
implementation(project(":core:analytics"))
implementation(project(":core:featuretoggles"))
@@ -23,6 +24,7 @@ dependencies {
implementation(project(":libs:auth"))
/** Features */
+ implementation(project(":features:onboarding"))
implementation(project(":features:referral:presentation"))
implementation(project(":features:referral:domain"))
implementation(project(":features:referral:data"))
diff --git a/app/src/main/assets/testnet_tokens.json b/app/src/main/assets/testnet_tokens.json
index 1e44bd5dfd..d36455384c 100644
--- a/app/src/main/assets/testnet_tokens.json
+++ b/app/src/main/assets/testnet_tokens.json
@@ -473,6 +473,17 @@
"networkId": "kava/test"
}
]
+ },
+ {
+ "id": "ravencoin",
+ "symbol": "RVN",
+ "name": "Ravencoin",
+ "networks":
+ [
+ {
+ "networkId": "ravencoin/test"
+ }
+ ]
}
]
}
diff --git a/app/src/main/java/com/tangem/tap/TapApplication.kt b/app/src/main/java/com/tangem/tap/TapApplication.kt
index 87f540fc6b..f8efc046b2 100644
--- a/app/src/main/java/com/tangem/tap/TapApplication.kt
+++ b/app/src/main/java/com/tangem/tap/TapApplication.kt
@@ -49,6 +49,7 @@ import com.tangem.tap.domain.walletStores.repository.WalletManagersRepository
import com.tangem.tap.domain.walletStores.repository.WalletStoresRepository
import com.tangem.tap.domain.walletStores.repository.di.provideDefaultImplementation
import com.tangem.tap.domain.walletconnect.WalletConnectRepository
+import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.proxy.AppStateHolder
@@ -130,6 +131,9 @@ class TapApplication : Application(), ImageLoaderFactory {
@Inject
lateinit var tokensListFeatureToggles: TokensListFeatureToggles
+ @Inject
+ lateinit var customTokenFeatureToggles: CustomTokenFeatureToggles
+
override fun onCreate() {
super.onCreate()
@@ -179,6 +183,7 @@ class TapApplication : Application(), ImageLoaderFactory {
assetReader = assetReader,
networkConnectionManager = networkConnectionManager,
tokensListFeatureToggles = tokensListFeatureToggles,
+ customTokenFeatureToggles = customTokenFeatureToggles,
),
)
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt b/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt
deleted file mode 100644
index a2eecd43e4..0000000000
--- a/app/src/main/java/com/tangem/tap/common/analytics/TangemSdkErrorMapper.kt
+++ /dev/null
@@ -1,129 +0,0 @@
-package com.tangem.tap.common.analytics
-
-import com.tangem.common.core.TangemSdkError
-
-object TangemSdkErrorMapper {
-
- // This mapping is performed to group errors in FirebaseCrashlytics.
- // At the moment, the errors in Crashlytics can only be grouped by their place of creation (class and line).
- @Suppress("LongMethod", "ComplexMethod")
- fun map(error: TangemSdkError): TangemSdkError {
- return when (error) {
- is TangemSdkError.TagLost -> TangemSdkError.TagLost()
- is TangemSdkError.ExtendedLengthNotSupported -> TangemSdkError.ExtendedLengthNotSupported()
- is TangemSdkError.SerializeCommandError -> TangemSdkError.SerializeCommandError()
- is TangemSdkError.DeserializeApduFailed -> TangemSdkError.DeserializeApduFailed()
- is TangemSdkError.EncodingFailedTypeMismatch -> TangemSdkError.EncodingFailedTypeMismatch(
- error.customMessage,
- )
- is TangemSdkError.EncodingFailed -> TangemSdkError.EncodingFailed(error.customMessage)
- is TangemSdkError.DecodingFailedMissingTag -> TangemSdkError.DecodingFailedMissingTag(
- error.customMessage,
- )
- is TangemSdkError.DecodingFailedTypeMismatch -> TangemSdkError.DecodingFailedTypeMismatch(
- error.customMessage,
- )
- is TangemSdkError.DecodingFailed -> TangemSdkError.DecodingFailed(error.customMessage)
- is TangemSdkError.InvalidResponse -> TangemSdkError.InvalidResponse()
- is TangemSdkError.UnknownStatus -> TangemSdkError.UnknownStatus(error.statusWord)
- is TangemSdkError.ErrorProcessingCommand -> TangemSdkError.ErrorProcessingCommand()
- is TangemSdkError.InvalidState -> TangemSdkError.InvalidState()
- is TangemSdkError.InsNotSupported -> TangemSdkError.InsNotSupported()
- is TangemSdkError.InvalidParams -> TangemSdkError.InvalidParams()
- is TangemSdkError.NeedEncryption -> TangemSdkError.NeedEncryption()
- is TangemSdkError.FileNotFound -> TangemSdkError.FileNotFound()
- is TangemSdkError.WalletNotFound -> TangemSdkError.WalletNotFound()
- is TangemSdkError.AlreadyPersonalized -> TangemSdkError.AlreadyPersonalized()
- is TangemSdkError.CannotBeDepersonalized -> TangemSdkError.CannotBeDepersonalized()
- is TangemSdkError.AccessCodeRequired -> TangemSdkError.AccessCodeRequired()
- is TangemSdkError.CardReadWrongWallet -> TangemSdkError.CardReadWrongWallet()
- is TangemSdkError.CardWithMaxZeroWallets -> TangemSdkError.CardWithMaxZeroWallets()
- is TangemSdkError.AlreadyCreated -> TangemSdkError.AlreadyCreated()
- is TangemSdkError.MaxNumberOfWalletsCreated -> TangemSdkError.MaxNumberOfWalletsCreated()
- is TangemSdkError.PurgeWalletProhibited -> TangemSdkError.PurgeWalletProhibited()
- is TangemSdkError.AccessCodeCannotBeChanged -> TangemSdkError.AccessCodeCannotBeChanged()
- is TangemSdkError.PasscodeCannotBeChanged -> TangemSdkError.PasscodeCannotBeChanged()
- is TangemSdkError.AccessCodeCannotBeDefault -> TangemSdkError.AccessCodeCannotBeDefault()
- is TangemSdkError.NoRemainingSignatures -> TangemSdkError.NoRemainingSignatures()
- is TangemSdkError.EmptyHashes -> TangemSdkError.EmptyHashes()
- is TangemSdkError.HashSizeMustBeEqual -> TangemSdkError.HashSizeMustBeEqual()
- is TangemSdkError.WalletIsNotCreated -> TangemSdkError.WalletIsNotCreated()
- is TangemSdkError.SignHashesNotAvailable -> TangemSdkError.SignHashesNotAvailable()
- is TangemSdkError.TooManyHashesInOneTransaction -> TangemSdkError.TooManyHashesInOneTransaction()
- is TangemSdkError.ExtendedDataSizeTooLarge -> TangemSdkError.ExtendedDataSizeTooLarge()
- is TangemSdkError.NotPersonalized -> TangemSdkError.NotPersonalized()
- is TangemSdkError.NotActivated -> TangemSdkError.NotActivated()
- is TangemSdkError.WalletIsPurged -> TangemSdkError.WalletIsPurged()
- is TangemSdkError.PasscodeRequired -> TangemSdkError.PasscodeRequired()
- is TangemSdkError.VerificationFailed -> TangemSdkError.VerificationFailed()
- is TangemSdkError.DataSizeTooLarge -> TangemSdkError.DataSizeTooLarge()
- is TangemSdkError.MissingCounter -> TangemSdkError.MissingCounter()
- is TangemSdkError.OverwritingDataIsProhibited -> TangemSdkError.OverwritingDataIsProhibited()
- is TangemSdkError.DataCannotBeWritten -> TangemSdkError.DataCannotBeWritten()
- is TangemSdkError.MissingIssuerPubicKey -> TangemSdkError.MissingIssuerPubicKey()
- is TangemSdkError.CardVerificationFailed -> TangemSdkError.CardVerificationFailed()
- is TangemSdkError.WrongAccessCode -> TangemSdkError.WrongAccessCode()
- is TangemSdkError.WrongPasscode -> TangemSdkError.WrongPasscode()
- is TangemSdkError.UnknownError -> TangemSdkError.UnknownError()
- is TangemSdkError.UserCancelled -> TangemSdkError.UserCancelled()
- is TangemSdkError.Busy -> TangemSdkError.Busy()
- is TangemSdkError.MissingPreflightRead -> TangemSdkError.MissingPreflightRead()
- is TangemSdkError.WrongCardNumber -> TangemSdkError.WrongCardNumber()
- is TangemSdkError.WrongCardType -> TangemSdkError.WrongCardType(null)
- is TangemSdkError.CardError -> TangemSdkError.CardError()
- is TangemSdkError.NotSupportedFirmwareVersion -> TangemSdkError.NotSupportedFirmwareVersion()
- is TangemSdkError.WalletError -> TangemSdkError.WalletError()
- is TangemSdkError.WalletCannotBeCreated -> TangemSdkError.WalletCannotBeCreated()
- is TangemSdkError.UnsupportedCurve -> TangemSdkError.UnsupportedCurve()
- is TangemSdkError.UnsupportedWalletConfig -> TangemSdkError.UnsupportedWalletConfig()
- is TangemSdkError.CryptoUtilsError -> TangemSdkError.CryptoUtilsError(error.customMessage)
- is TangemSdkError.NetworkError -> TangemSdkError.NetworkError(error.customMessage)
- is TangemSdkError.ExceptionError -> TangemSdkError.ExceptionError(error.cause)
- is TangemSdkError.TooMuchBackupCards -> TangemSdkError.TooMuchBackupCards()
- is TangemSdkError.BackupCardRequired -> TangemSdkError.BackupCardRequired()
- is TangemSdkError.CertificateSignatureRequired -> TangemSdkError.CertificateSignatureRequired()
- is TangemSdkError.AccessCodeOrPasscodeRequired -> TangemSdkError.AccessCodeOrPasscodeRequired()
- is TangemSdkError.ResetPinNoCardsToReset -> TangemSdkError.ResetPinNoCardsToReset()
- is TangemSdkError.ResetPinWrongCard -> TangemSdkError.ResetPinWrongCard()
- is TangemSdkError.BackupFailedCardNotLinked -> TangemSdkError.BackupFailedCardNotLinked()
- is TangemSdkError.BackupNotAllowed -> TangemSdkError.BackupNotAllowed()
- is TangemSdkError.BackupCardAlreadyAdded -> TangemSdkError.BackupCardAlreadyAdded()
- is TangemSdkError.MissingPrimaryCard -> TangemSdkError.MissingPrimaryCard()
- is TangemSdkError.MissingPrimaryAttestSignature -> TangemSdkError.MissingPrimaryAttestSignature()
- is TangemSdkError.NoBackupDataForCard -> TangemSdkError.NoBackupDataForCard()
- is TangemSdkError.BackupFailedEmptyWallets -> TangemSdkError.BackupFailedEmptyWallets()
- is TangemSdkError.BackupFailedNotEmptyWallets -> TangemSdkError.BackupFailedNotEmptyWallets()
- is TangemSdkError.NoActiveBackup -> TangemSdkError.NoActiveBackup()
- is TangemSdkError.BackupServiceInvalidState -> TangemSdkError.BackupServiceInvalidState()
- is TangemSdkError.NoBackupCardForIndex -> TangemSdkError.NoBackupCardForIndex()
- is TangemSdkError.EmptyBackupCards -> TangemSdkError.EmptyBackupCards()
- is TangemSdkError.BackupFailedWrongIssuer -> TangemSdkError.BackupFailedWrongIssuer()
- is TangemSdkError.BackupFailedHDWalletSettings -> TangemSdkError.BackupFailedHDWalletSettings()
- is TangemSdkError.BackupFailedNotEnoughCurves -> TangemSdkError.BackupFailedNotEnoughCurves()
- is TangemSdkError.BackupFailedNotEnoughWallets -> TangemSdkError.BackupFailedNotEnoughWallets()
- is TangemSdkError.FileSettingsUnsupported -> TangemSdkError.FileSettingsUnsupported()
- is TangemSdkError.FilesIsEmpty -> TangemSdkError.FilesIsEmpty()
- is TangemSdkError.FilesDisabled -> TangemSdkError.FilesDisabled()
- is TangemSdkError.HDWalletDisabled -> TangemSdkError.HDWalletDisabled()
- is TangemSdkError.WrongInteractionMode -> TangemSdkError.WrongInteractionMode()
- is TangemSdkError.IssuerSignatureLoadingFailed -> TangemSdkError.IssuerSignatureLoadingFailed()
- is TangemSdkError.BackupFailedFirmware -> TangemSdkError.BackupFailedFirmware()
- is TangemSdkError.UserForgotTheCode -> TangemSdkError.UserForgotTheCode()
- is TangemSdkError.BackupFailedIncompatibleBatch -> TangemSdkError.BackupFailedIncompatibleBatch()
- is TangemSdkError.BiometricsUnavailable -> error
- is TangemSdkError.BiometricsAuthenticationFailed -> error
- is TangemSdkError.BiometricsAuthenticationLockout -> error
- is TangemSdkError.BiometricsAuthenticationPermanentLockout -> error
- is TangemSdkError.UserCanceledBiometricsAuthentication -> error
- is TangemSdkError.EncryptionOperationFailed -> error
- is TangemSdkError.InvalidEncryptionKey -> error
- is TangemSdkError.KeyGenerationException -> error
- is TangemSdkError.MnemonicException -> error
- is TangemSdkError.WalletAlreadyCreated -> error
- is TangemSdkError.ResetBackupFailedHasBackedUpWallets -> error
- is TangemSdkError.KeysImportDisabled -> error
- is TangemSdkError.Underlying -> error
- is TangemSdkError.UserCodeRecoveryDisabled -> error
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt
index 8bbf95d217..05fd6ae43c 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/AnalyticsParam.kt
@@ -59,6 +59,20 @@ sealed class AnalyticsParam {
}
}
+ sealed class AccessCodeRecoveryStatus(val value: String) {
+
+ val key: String = "Status"
+
+ object Enabled : AccessCodeRecoveryStatus("Enabled")
+ object Disabled : AccessCodeRecoveryStatus("Disabled")
+
+ companion object {
+ fun from(enabled: Boolean): AccessCodeRecoveryStatus {
+ return if (enabled) Enabled else Disabled
+ }
+ }
+ }
+
sealed class Error(val value: String) {
object App : Error("App Error")
object CardSdk : Error("Card Sdk Error")
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt
index 55ed5f97b9..8e0b8cc5b9 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/ManageTokens.kt
@@ -35,12 +35,40 @@ sealed class ManageTokens(
params: Map = mapOf(),
) : ManageTokens(event, params) {
- class ScreenOpened : ManageTokens("Custom Token Screen Opened")
+ object ScreenOpened : ManageTokens(event = "Custom Token Screen Opened")
- class TokenWasAdded(customCurrency: CustomCurrency) : ManageTokens(
+ // TODO("Get rid of strong binding (CustomCurrency)
+ open class TokenWasAdded(customCurrency: CustomCurrency) : ManageTokens(
event = "Custom Token Was Added",
params = convertToParam(customCurrency),
) {
+
+ data class Token(
+ val symbol: String,
+ val derivationPath: String?,
+ val blockchain: com.tangem.blockchain.common.Blockchain,
+ val contractAddress: String,
+ ) : ManageTokens(
+ event = "Custom Token Was Added",
+ params = mapOf(
+ "Token" to symbol,
+ "Derivation Path" to derivationPath,
+ "Network Id" to blockchain.currency,
+ "Contract Address" to contractAddress,
+ ).filterNotNull(),
+ )
+
+ data class Blockchain(
+ val derivationPath: String?,
+ val blockchain: com.tangem.blockchain.common.Blockchain,
+ ) : ManageTokens(
+ event = "Custom Token Was Added",
+ params = mapOf(
+ "Token" to blockchain.currency,
+ "Derivation Path" to derivationPath,
+ ).filterNotNull(),
+ )
+
companion object {
private fun convertToParam(customCurrency: CustomCurrency): Map = with(customCurrency) {
return when (this) {
@@ -48,6 +76,7 @@ sealed class ManageTokens(
"Token" to network.currency,
"Derivation Path" to derivationPath?.rawPath,
).filterNotNull()
+
is CustomCurrency.CustomToken -> mapOf(
"Token" to token.symbol,
"Derivation Path" to derivationPath?.rawPath,
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt
index 8a2fceadaf..5a9226bec4 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/events/Settings.kt
@@ -53,6 +53,13 @@ sealed class Settings(
params = mapOf("Mode" to mode.value),
error = error,
)
+
+ class AccessCodeRecoveryButton : CardSettings("Button - Access Code Recovery")
+
+ class AccessCodeRecoveryChanged(status: AnalyticsParam.AccessCodeRecoveryStatus) : CardSettings(
+ event = "Access Code Recovery Changed",
+ params = mapOf(status.key to status.value),
+ )
}
sealed class AppSettings(
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt
index 5c087a4f20..0f9b7c6bc1 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/CardContextInterceptor.kt
@@ -2,8 +2,9 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.ParamsInterceptor
-import com.tangem.domain.common.ProductType
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ProductType
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.IntroductionProcess
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt
index 9dcfd80714..94c006d699 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/paramsInterceptor/LinkedCardContextInterceptor.kt
@@ -2,7 +2,7 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.ParamsInterceptor
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
/**
[REDACTED_AUTHOR]
diff --git a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt
index 2435646fed..30caf9f355 100644
--- a/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt
+++ b/app/src/main/java/com/tangem/tap/common/analytics/topup/TopUpController.kt
@@ -4,8 +4,9 @@ import com.tangem.common.extensions.guard
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.CardTypesResolver
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.UserWalletId
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.converters.TopUpEventConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.extensions.copy
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 e762a3dabe..617f2f2c46 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
@@ -36,7 +36,7 @@ import com.tangem.domain.redux.domainStore
import com.tangem.domain.redux.global.DomainGlobalAction
import com.tangem.domain.redux.global.DomainGlobalState
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
-import com.tangem.tap.features.addCustomToken.compose.SelectTokenNetworkDialog
+import com.tangem.tap.features.customtoken.legacy.compose.SelectTokenNetworkDialog
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt
index 8d3ce8e5ad..8e79d07381 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/Analytics.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.common.extensions
import com.tangem.core.analytics.Analytics
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.paramsInterceptor.LinkedCardContextInterceptor
/**
diff --git a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt
index c8b2e0e8d2..5aae0ce706 100644
--- a/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt
+++ b/app/src/main/java/com/tangem/tap/common/extensions/Blockchain.kt
@@ -39,6 +39,7 @@ fun Blockchain.getGreyedOutIconRes(): Int {
Blockchain.Kaspa -> R.drawable.ic_kaspa_no_color
Blockchain.TON, Blockchain.TONTestnet -> R.drawable.ic_ton_no_color
Blockchain.Kava, Blockchain.KavaTestnet -> R.drawable.ic_kava_no_color
+ Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> R.drawable.ic_ravencoin_no_color
else -> R.drawable.ic_tangem_logo
}
}
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 7ad24431c6..efd80355aa 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
@@ -9,9 +9,10 @@ import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.FragmentShareTransition
-import com.tangem.tap.features.addCustomToken.AddCustomTokenFragment
+import com.tangem.tap.features.customtoken.legacy.AddCustomTokenFragment
import com.tangem.tap.features.details.ui.appsettings.AppSettingsFragment
import com.tangem.tap.features.details.ui.cardsettings.CardSettingsFragment
+import com.tangem.tap.features.details.ui.cardsettings.coderecovery.AccessCodeRecoveryFragment
import com.tangem.tap.features.details.ui.details.DetailsFragment
import com.tangem.tap.features.details.ui.resetcard.ResetCardFragment
import com.tangem.tap.features.details.ui.securitymode.SecurityModeFragment
@@ -36,6 +37,7 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.tap.store
import com.tangem.wallet.R
import timber.log.Timber
+import com.tangem.tap.features.customtoken.impl.presentation.AddCustomTokenFragment as RedesignedAddCustomTokenFragment
fun FragmentActivity.openFragment(
screen: AppScreen,
@@ -109,6 +111,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.CardSettings -> CardSettingsFragment()
AppScreen.AppSettings -> AppSettingsFragment()
AppScreen.ResetToFactory -> ResetCardFragment()
+ AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.AddTokens -> {
val featureToggles = store.state.daggerGraphState.get(
@@ -116,7 +119,16 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
)
if (featureToggles.isRedesignedScreenEnabled) TokensListFragment() else AddTokensFragment()
}
- AppScreen.AddCustomToken -> AddCustomTokenFragment()
+ AppScreen.AddCustomToken -> {
+ val featureToggles = store.state.daggerGraphState.get(
+ getDependency = DaggerGraphState::customTokenFeatureToggles,
+ )
+ if (featureToggles.isRedesignedScreenEnabled) {
+ RedesignedAddCustomTokenFragment()
+ } else {
+ AddCustomTokenFragment()
+ }
+ }
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScan -> QrScanFragment()
diff --git a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt
index c6f5bafd0a..7d138f1610 100644
--- a/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt
+++ b/app/src/main/java/com/tangem/tap/common/feedback/AdditionalFeedbackInfo.kt
@@ -8,8 +8,8 @@ import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.Address
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
diff --git a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt
index 8de0b8e686..6fddf99607 100644
--- a/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt
+++ b/app/src/main/java/com/tangem/tap/common/feedback/FeedbackManager.kt
@@ -25,7 +25,7 @@ class FeedbackManager(
fun sendEmail(feedbackData: FeedbackData, onFail: ((Exception) -> Unit)? = null) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
- val fileLog = if (feedbackData is ScanFailsEmail) createLogFile(activity) else null
+ val fileLog = createLogFile(activity)
activity.sendEmail(
email = getSupportEmail(),
subject = activity.getString(feedbackData.subjectResId),
diff --git a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt
index e9aaabfd70..a96f1c1e06 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/AccessCodeRequestPolicyMiddleware.kt
@@ -1,6 +1,6 @@
package com.tangem.tap.common.redux
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.preferencesStorage
import com.tangem.tap.tangemSdkManager
diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt
index a140fb2d0c..f589ca6743 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalAction.kt
@@ -6,7 +6,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemError
import com.tangem.datasource.config.ConfigManager
import com.tangem.datasource.config.models.ChatConfig
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackData
diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt
index 5d99c5357e..39d29a451f 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalMiddleware.kt
@@ -4,11 +4,12 @@ import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.datasource.config.models.Config
-import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.LogConfig
-import com.tangem.domain.common.ProductType
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ProductType
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
diff --git a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt
index 1d6dddf647..c9f2314efc 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/global/GlobalState.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.common.redux.global
import com.tangem.datasource.config.ConfigManager
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.topup.TopUpController
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.feedback.FeedbackManager
diff --git a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
index 0b40b7bd8e..861a0a0fb9 100644
--- a/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
+++ b/app/src/main/java/com/tangem/tap/common/redux/navigation/NavigationState.kt
@@ -18,7 +18,7 @@ enum class AppScreen(
OnboardingNote, OnboardingWallet, OnboardingTwins, OnboardingOther,
Wallet, WalletDetails,
Send,
- Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory,
+ Details, DetailsSecurity, CardSettings, AppSettings, ResetToFactory, AccessCodeRecovery,
AddTokens, AddCustomToken,
WalletConnectSessions,
QrScan,
diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
index c9d4a5e39c..c0ffba9893 100644
--- a/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TangemSdkManager.kt
@@ -22,15 +22,16 @@ import com.tangem.common.map
import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.core.analytics.Analytics
import com.tangem.crypto.hdWallet.DerivationPath
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
-import com.tangem.operations.CommandResponse
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.derivation.DeriveMultipleWalletPublicKeysTask
import com.tangem.operations.pins.CheckUserCodesCommand
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.operations.pins.SetUserCodeCommand
+import com.tangem.operations.usersetttings.SetUserCodeRecoveryAllowedTask
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.domain.tasks.CreateWalletAndRescanTask
import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
@@ -173,6 +174,14 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
)
}
+ suspend fun setAccessCodeRecoveryEnabled(cardId: String?, enabled: Boolean): CompletionResult {
+ return runTaskAsyncReturnOnMain(
+ SetUserCodeRecoveryAllowedTask(enabled),
+ cardId,
+ initialMessage = Message(context.getString(R.string.initial_message_tap_header)),
+ )
+ }
+
suspend fun scanCard(
cardId: String? = null,
allowRequestAccessCodeFromRepository: Boolean = false,
@@ -185,7 +194,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
.map { CardDTO(it) }
}
- suspend fun runTaskAsync(
+ suspend fun runTaskAsync(
runnable: CardSessionRunnable,
cardId: String? = null,
initialMessage: Message? = null,
@@ -198,7 +207,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
}
}
- private suspend fun runTaskAsyncReturnOnMain(
+ private suspend fun runTaskAsyncReturnOnMain(
runnable: CardSessionRunnable,
cardId: String? = null,
initialMessage: Message? = null,
diff --git a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
index 196ec2f0f4..f316b2e433 100644
--- a/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TangemSigner.kt
@@ -5,7 +5,8 @@ import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.common.TapWorkarounds.isTangemTwins
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
@@ -23,12 +24,12 @@ class TangemSigner(
publicKey: Wallet.PublicKey,
): CompletionResult> {
return suspendCancellableCoroutine { continuation ->
- val cardId = if (card.backupStatus?.isActive == true) null else card.cardId
-
+ val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val task = SignHashesTask(hashes, publicKey)
+
tangemSdk.startSessionWithRunnable(
runnable = task,
- cardId = cardId,
+ cardId = card.cardId.takeIf { isCardNotBackedUp },
initialMessage = initialMessage,
accessCode = accessCode,
) { result ->
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 449192d23a..fc1d4e3958 100644
--- a/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/TapWalletManager.kt
@@ -1,16 +1,13 @@
package com.tangem.tap.domain
-import com.tangem.blockchain.common.Blockchain
-import com.tangem.blockchain.common.BlockchainSdkConfig
-import com.tangem.blockchain.common.Token
-import com.tangem.blockchain.common.Wallet
-import com.tangem.blockchain.common.WalletManagerFactory
+import com.tangem.blockchain.common.*
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.datasource.config.ConfigManager
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.Attestation
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.extensions.dispatchOnMain
@@ -38,6 +35,9 @@ class TapWalletManager {
}
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) {
+ // do nothing if its the same wallet
+ if (store.state.globalState.scanResponse == userWallet.scanResponse) return
+
Analytics.setContext(userWallet.scanResponse)
if (sendAnalyticsEvent) {
Analytics.send(Basic.WalletOpened())
diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
index 005f05500c..19bbc1ae52 100644
--- a/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
+++ b/app/src/main/java/com/tangem/tap/domain/extensions/Card.kt
@@ -3,7 +3,7 @@ package com.tangem.tap.domain.extensions
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.extensions.toHexString
import com.tangem.common.services.Result
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
diff --git a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt
index b2dbd64995..a9de7cb47a 100644
--- a/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt
+++ b/app/src/main/java/com/tangem/tap/domain/extensions/WalletManagerFactory.kt
@@ -9,10 +9,11 @@ import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
diff --git a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
index a76fbb9d1a..d58a3e7122 100644
--- a/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
+++ b/app/src/main/java/com/tangem/tap/domain/model/UserWallet.kt
@@ -1,6 +1,6 @@
package com.tangem.tap.domain.model
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.util.UserWalletId
/**
diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt
index 3a82b45171..3cc1d2e5f5 100644
--- a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt
+++ b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletBuilder.kt
@@ -1,9 +1,10 @@
package com.tangem.tap.domain.model.builders
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ProductType
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ProductType
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.userWalletList.GetCardImageUseCase
diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt
index 70b7f005b5..68626b34eb 100644
--- a/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt
+++ b/app/src/main/java/com/tangem/tap/domain/model/builders/UserWalletIdBuilder.kt
@@ -3,9 +3,9 @@ package com.tangem.tap.domain.model.builders
import com.tangem.common.extensions.calculateSha256
import com.tangem.common.extensions.hexToBytes
import com.tangem.crypto.Secp256k1
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ProductType
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ProductType
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.extensions.calculateHmacSha256
import com.tangem.domain.common.util.UserWalletId
diff --git a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt
index d9805be40f..ecca6f2314 100644
--- a/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt
+++ b/app/src/main/java/com/tangem/tap/domain/model/builders/WalletStoreBuilder.kt
@@ -8,6 +8,7 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.TapWorkarounds.derivationStyle
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.extensions.getBlockchainTxHistory
import com.tangem.tap.common.extensions.getTokenTxHistory
import com.tangem.tap.domain.model.UserWallet
diff --git a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt
index 7e0f77f2d1..86f790ee67 100644
--- a/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt
+++ b/app/src/main/java/com/tangem/tap/domain/scanCard/ScanCardProcessor.kt
@@ -7,8 +7,10 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.AnalyticsEvent
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.domain.common.util.twinsIsTwinned
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.backup.BackupService
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.backupService
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
index e57a629edc..75ea69067b 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/CreateProductWalletTask.kt
@@ -12,11 +12,11 @@ import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.common.map
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.CardTypesResolver
-import com.tangem.domain.common.KeyWalletPublicKey
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
+import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.operations.CommandResponse
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt
index 474bbd49dd..5251c0b0a1 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ProductCommandProcessor.kt
@@ -2,7 +2,7 @@ package com.tangem.tap.domain.tasks.product
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
/**
[REDACTED_AUTHOR]
diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt
index 78866f673b..8f7fe94bac 100644
--- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt
@@ -15,16 +15,17 @@ import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toByteArray
import com.tangem.common.extensions.toHexString
import com.tangem.common.extensions.toMapKey
-import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ProductType
-import com.tangem.domain.common.ScanResponse
+import com.tangem.crypto.hdWallet.DerivationPath
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ProductType
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isExcluded
import com.tangem.domain.common.TapWorkarounds.isNotSupportedInThatRelease
import com.tangem.domain.common.TapWorkarounds.isSaltPay
+import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
import com.tangem.domain.common.TwinsHelper
@@ -155,7 +156,7 @@ private class ScanWalletProcessor(
CompletionResult.Success(
ScanResponse(
card = card,
- productType = ProductType.Note,
+ productType = determineProductTypeForSingleCurrencyWallet(card),
walletData = walletData,
),
),
@@ -176,6 +177,14 @@ private class ScanWalletProcessor(
}
}
+ private fun determineProductTypeForSingleCurrencyWallet(card: CardDTO): ProductType {
+ return if (card.isStart2Coin) {
+ ProductType.Start2Coin
+ } else {
+ ProductType.Note
+ }
+ }
+
private fun createMissingWalletsIfNeeded(
card: CardDTO,
session: CardSession,
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt
index f4c06f518a..22baf587ab 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/CurrenciesRepository.kt
@@ -2,7 +2,7 @@ package com.tangem.tap.domain.tokens
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.FirmwareVersion
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
object CurrenciesRepository {
fun getBlockchains(cardFirmware: CardDTO.FirmwareVersion, isTestNet: Boolean = false): List {
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt
index 175f72a121..255d8611c5 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/UserTokensRepository.kt
@@ -7,7 +7,7 @@ import com.tangem.datasource.api.tangemTech.TangemTechApi
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.datasource.api.tangemTech.models.UserTokensResponse
import com.tangem.datasource.connection.NetworkConnectionManager
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.AndroidFileReader
import com.tangem.tap.domain.model.builders.UserWalletIdBuilder
import com.tangem.tap.domain.tokens.converters.CurrencyConverter
diff --git a/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt b/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt
index 8abb99b14d..32cfbc4bec 100644
--- a/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt
+++ b/app/src/main/java/com/tangem/tap/domain/tokens/models/BlockchainNetwork.kt
@@ -5,7 +5,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.calculateHashCode
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.TapWorkarounds.derivationStyle
@JsonClass(generateAdapter = true)
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt
index 127c10ec33..1ece7060b3 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/FinalizeTwinTask.kt
@@ -4,7 +4,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.KeyPair
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
diff --git a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt
index 3795a0cf00..6ef5f2bbe9 100644
--- a/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/twins/TwinCardsManager.kt
@@ -10,8 +10,8 @@ import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.tap.tangemSdkManager
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt
index 03ec981f62..ec9ed95b1d 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/UserWalletsListError.kt
@@ -4,6 +4,7 @@ import com.tangem.common.core.TangemError
import com.tangem.wallet.R
sealed class UserWalletsListError(code: Int) : TangemError(code) {
+
override val silent: Boolean
get() = (cause as? TangemError)?.silent == true
@@ -14,8 +15,12 @@ sealed class UserWalletsListError(code: Int) : TangemError(code) {
override val messageResId: Int = R.string.user_wallet_list_error_wallet_already_saved
}
- object InvalidEncryptionKey : UserWalletsListError(code = 60002) {
- override var customMessage: String = "Invalid encryption key"
+ object EncryptionKeyInvalidated : UserWalletsListError(code = 60002) {
+ override var customMessage: String = "Encryption key invalidated"
+ }
+
+ object BiometricsAuthenticationDisabled : UserWalletsListError(code = 60005) {
+ override var customMessage: String = "Biometrics authentication disabled"
}
data class BiometricsAuthenticationLockout(val isPermanent: Boolean) : UserWalletsListError(code = 60003) {
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt
index 76f11e8ae0..71255de6a0 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerProvider.kt
@@ -17,6 +17,7 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUse
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.json.ByteArrayKeyAdapter
import com.tangem.tap.domain.userWalletList.utils.json.CardBackupStatusAdapter
+import com.tangem.tap.domain.userWalletList.utils.json.DerivationPathAdapterWithMigration
import com.tangem.tap.domain.userWalletList.utils.json.ExtendedPublicKeysMapAdapter
import com.tangem.tap.domain.userWalletList.utils.json.ScanResponseDerivedKeysMapAdapter
import com.tangem.tap.domain.userWalletList.utils.json.WalletDerivedKeysMapAdapter
@@ -33,9 +34,10 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation(
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
+ .add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
- .add(TangemSdkAdapter.DerivationPathAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
+ .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(KotlinJsonAdapterFactory())
.build()
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt
index 72244a8bf8..ad29020162 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/implementation/BiometricUserWalletsListManager.kt
@@ -183,7 +183,11 @@ internal class BiometricUserWalletsListManager(
return saveEncryptionKeyIfNotNull(userWallet)
.flatMap { sensitiveInformationRepository.save(userWallet, encryptionKey = it) }
.flatMap { publicInformationRepository.save(userWallet) }
- .map { selectedUserWalletRepository.set(userWallet.walletId) }
+ .map {
+ if (changeSelectedUserWallet) {
+ selectedUserWalletRepository.set(userWallet.walletId)
+ }
+ }
.flatMap { loadModels() }
.doOnSuccess {
state.update { prevState ->
@@ -259,7 +263,7 @@ internal class BiometricUserWalletsListManager(
}
}
.doOnFailure { error ->
- if (error is UserWalletsListError.InvalidEncryptionKey) {
+ if (error is UserWalletsListError.EncryptionKeyInvalidated) {
state.update { prevState ->
prevState.copy(
hasLockedUserWalletsAfterUnlock = true,
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt
index d383e11e3f..d81f8ebafc 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/model/UserWalletInformation.kt
@@ -1,8 +1,8 @@
package com.tangem.tap.domain.userWalletList.model
import com.squareup.moshi.JsonClass
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.util.UserWalletId
@JsonClass(generateAdapter = true)
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt
index 0307b8b7d4..affeeeef57 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/repository/implementation/BiometricUserWalletsKeysRepository.kt
@@ -46,7 +46,10 @@ internal class BiometricUserWalletsKeysRepository(
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = false)
is TangemSdkError.BiometricsAuthenticationPermanentLockout ->
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true)
- is TangemSdkError.InvalidEncryptionKey -> UserWalletsListError.InvalidEncryptionKey
+ is TangemSdkError.BiometricCryptographyKeyInvalidated ->
+ UserWalletsListError.EncryptionKeyInvalidated
+ is TangemSdkError.BiometricsUnavailable ->
+ UserWalletsListError.BiometricsAuthenticationDisabled
else -> error
}
}
@@ -91,28 +94,36 @@ internal class BiometricUserWalletsKeysRepository(
private suspend fun getAllInternal(): CompletionResult> {
return getUserWalletsIds()
.map { userWalletId ->
- // This is possible because the Card SDK cipher key has an expiration time
- // If this operation runs more than that expiration time, the user will have to re-authorize
- // to receive all keys
+ // It is possible to request multiple user wallet keys from biometric storage because
+ // the biometric cryptography key has an expiration time.
+ // If this operation runs more than that expiration time, then the user will have to re-authorize
+ // to receive all user wallets encryption keys
getEncryptionKey(userWalletId)
.flatMapOnFailure { error ->
- // If key decryption failed then skip it
- if (error is TangemSdkError.EncryptionOperationFailed) {
- CompletionResult.Success(data = null)
- } else {
- CompletionResult.Failure(error)
+ when (error) {
+ is TangemSdkError.InvalidBiometricCryptographyKey,
+ is TangemSdkError.BiometricCryptographyOperationFailed,
+ -> {
+ // These errors can be skipped as the user has the option to re-save their wallets
+ // in case they occur
+ CompletionResult.Success(data = null)
+ }
+ else -> CompletionResult.Failure(error)
}
}
.doOnFailure { error ->
when (error) {
is TangemSdkError.UserCanceledBiometricsAuthentication -> {
- // If the user cancels biometric authentication, cancel the request for all keys
+ // If the user cancels biometric authentication, then cancel operation with error
return CompletionResult.Failure(error)
}
- is TangemSdkError.InvalidEncryptionKey -> {
- if (error.isKeyRegenerated) {
+ is TangemSdkError.BiometricCryptographyKeyInvalidated -> {
+ // If the biometric cryptography key was invalidated,
+ // then delete all user wallets encryption keys and cancel operation with error
+ getUserWalletsIds().forEach { userWalletId ->
deleteEncryptionKey(userWalletId)
}
+ return CompletionResult.Failure(error)
}
}
}
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt
index d97af026cc..f58758f35a 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/UserWalletEncyptionKeyCalculator.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.domain.userWalletList.utils
import com.tangem.common.extensions.calculateSha256
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.extensions.calculateHmacSha256
internal val CardDTO.encryptionKey: ByteArray?
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/CardBackupStatusAdapter.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/CardBackupStatusAdapter.kt
index 2f7e2544f5..dc40917fb8 100644
--- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/CardBackupStatusAdapter.kt
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/CardBackupStatusAdapter.kt
@@ -5,7 +5,7 @@ import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter
import com.squareup.moshi.ToJson
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
internal class CardBackupStatusAdapter {
@ToJson
diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/DerivationPathAdapterWithMigration.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/DerivationPathAdapterWithMigration.kt
new file mode 100644
index 0000000000..4719b96b49
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/json/DerivationPathAdapterWithMigration.kt
@@ -0,0 +1,35 @@
+package com.tangem.tap.domain.userWalletList.utils.json
+
+import com.squareup.moshi.FromJson
+import com.squareup.moshi.ToJson
+import com.tangem.common.extensions.guard
+import com.tangem.common.json.MoshiJsonConverter
+import com.tangem.crypto.hdWallet.DerivationNode
+import com.tangem.crypto.hdWallet.DerivationPath
+import timber.log.Timber
+
+class DerivationPathAdapterWithMigration {
+ @ToJson
+ fun toJson(src: DerivationPath): String = src.rawPath
+
+ @FromJson
+ fun fromJson(json: String): DerivationPath {
+ val jsonMap = MoshiJsonConverter.default().toMap(json)
+ return if (jsonMap.isEmpty()) {
+ DerivationPath(json)
+ } else {
+ fromLegacyScheme(jsonMap)
+ }
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ private fun fromLegacyScheme(jsonMap: Map): DerivationPath {
+ val rawPath = jsonMap["rawPath"] as String
+ val nodeIndexes = (jsonMap["nodes"] as? List).guard {
+ Timber.e("Unable to convert derivation path nodes from JSON")
+ return DerivationPath(rawPath)
+ }
+ val nodes = nodeIndexes.map { DerivationNode.fromIndex(it.toLong()) }
+ return DerivationPath(rawPath, nodes)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt
index 2281fbfe5f..f344f46d4a 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletCurrencies/implementation/DefaultWalletCurrenciesManager.kt
@@ -5,7 +5,7 @@ import com.tangem.common.CompletionResult
import com.tangem.common.flatMap
import com.tangem.common.fold
import com.tangem.common.map
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.entities.FiatCurrency
diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt
index 30de69eec5..6b2aa5ccff 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletAmountsRepository.kt
@@ -17,8 +17,9 @@ import com.tangem.common.flatMapOnFailure
import com.tangem.common.fold
import com.tangem.common.map
import com.tangem.datasource.api.tangemTech.TangemTechApi
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.common.util.UserWalletId
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.replaceByOrAdd
diff --git a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt
index 6c5eba1809..b2d8167a7e 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletStores/repository/implementation/DefaultWalletManagersRepository.kt
@@ -9,13 +9,14 @@ import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.common.CompletionResult
import com.tangem.common.catching
import com.tangem.common.doOnSuccess
-import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.common.mapFailure
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
+import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.TapWorkarounds.useOldStyleDerivation
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.UserWalletId
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.extensions.makeWalletManagerForApp
import com.tangem.tap.domain.model.UserWallet
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt
index 1cae897360..64037f50e2 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectManager.kt
@@ -4,7 +4,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.events.WalletConnect
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
diff --git a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt
index b963a290d4..5ccf53357d 100644
--- a/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt
+++ b/app/src/main/java/com/tangem/tap/domain/walletconnect/WalletConnectSdkHelper.kt
@@ -63,26 +63,24 @@ class WalletConnectSdkHelper {
val wallet = walletManager.wallet
val balance = wallet.amounts[AmountType.Coin]?.value ?: return null
- val gas = transaction.gas?.hexToBigDecimal()
- ?: transaction.gasLimit?.hexToBigDecimal()
- ?: BigDecimal(300000) // Set high gasLimit if not provided
-
val decimals = wallet.blockchain.decimals()
val value = (transaction.value ?: "0").hexToBigDecimal()
?.movePointLeft(decimals) ?: return null
+ val gasLimit = getGasLimitFromTx(value, walletManager, transaction)
+
val gasPrice = transaction.gasPrice?.hexToBigDecimal()
?: when (val result = (walletManager as? EthereumGasLoader)?.getGasPrice()) {
is Result.Success -> result.data.toBigDecimal()
is Result.Failure -> {
- (result.error as? Throwable)?.let { Timber.e(it) }
+ (result.error as? Throwable)?.let { Timber.e(it, "getGasPrice failed") }
return null
}
null -> return null
}
- val fee = (gas * gasPrice).movePointLeft(decimals)
+ val fee = (gasLimit * gasPrice).movePointLeft(decimals)
val total = value + fee
val transactionData = TransactionData(
@@ -92,7 +90,7 @@ class WalletConnectSdkHelper {
destinationAddress = transaction.to!!,
extras = EthereumTransactionExtras(
data = transaction.data.removePrefix(HEX_PREFIX).hexToBytes(),
- gasLimit = gas.toBigInteger(),
+ gasLimit = gasLimit.toBigInteger(),
nonce = transaction.nonce?.hexToBigDecimal()?.toBigInteger(),
),
)
@@ -139,6 +137,40 @@ class WalletConnectSdkHelper {
}
}
+ private suspend fun getGasLimitFromTx(
+ value: BigDecimal,
+ walletManager: WalletManager,
+ transaction: WCEthereumTransaction,
+ ): BigDecimal {
+ return transaction.gas?.hexToBigDecimal()
+ ?: transaction.gasLimit?.hexToBigDecimal()
+ ?: getGaLimitFromBlockchain(
+ value = value,
+ walletManager = walletManager,
+ transaction = transaction,
+ )
+ }
+
+ private suspend fun getGaLimitFromBlockchain(
+ value: BigDecimal,
+ walletManager: WalletManager,
+ transaction: WCEthereumTransaction,
+ ): BigDecimal {
+ val gasLimitResult = (walletManager as? EthereumGasLoader)?.getGasLimit(
+ amount = Amount(value, walletManager.wallet.blockchain),
+ destination = transaction.to ?: "",
+ data = transaction.data,
+ )
+ return when (gasLimitResult) {
+ is Result.Success -> gasLimitResult.data.toBigDecimal().multiply(BigDecimal("1.2"))
+ is Result.Failure -> {
+ (gasLimitResult.error as? Throwable)?.let { Timber.e(it, "getGasLimit failed") }
+ BigDecimal(DEFAULT_MAX_GASLIMIT) // Set high gasLimit if not provided
+ }
+ else -> BigDecimal(DEFAULT_MAX_GASLIMIT) // Set high gasLimit if not provided
+ }
+ }
+
private suspend fun sendTransaction(data: WcTransactionData, cardId: String?): String? {
val result = (data.walletManager as TransactionSender).send(
transactionData = data.transaction,
@@ -290,6 +322,7 @@ class WalletConnectSdkHelper {
companion object {
private const val ETH_MESSAGE_PREFIX = "\u0019Ethereum Signed Message:\n"
private const val HEX_PREFIX = "0x"
+ private const val DEFAULT_MAX_GASLIMIT = 350000
fun getBnbResultString(publicKey: String, signature: String): String {
return "{\"signature\":\"$signature\",\"publicKey\":\"$publicKey\"}"
}
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt
new file mode 100644
index 0000000000..6de26f4bd2
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/api/featuretoggles/CustomTokenFeatureToggles.kt
@@ -0,0 +1,12 @@
+package com.tangem.tap.features.customtoken.api.featuretoggles
+
+/**
+ * Add custom token feature toggles
+ *
+[REDACTED_AUTHOR]
+ */
+interface CustomTokenFeatureToggles {
+
+ /** Availability of redesigned screen (internal feature) */
+ val isRedesignedScreenEnabled: Boolean
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt
new file mode 100644
index 0000000000..119872604d
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/DefaultCustomTokenRepository.kt
@@ -0,0 +1,57 @@
+package com.tangem.tap.features.customtoken.impl.data
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.datasource.api.tangemTech.TangemTechApi
+import com.tangem.domain.common.extensions.supportedBlockchains
+import com.tangem.domain.common.extensions.toNetworkId
+import com.tangem.tap.features.customtoken.impl.data.converters.FoundTokenConverter
+import com.tangem.tap.features.customtoken.impl.domain.CustomTokenRepository
+import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
+import com.tangem.tap.proxy.AppStateHolder
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import kotlinx.coroutines.withContext
+
+/**
+ * Default implementation of custom token repository
+ *
+ * @property tangemTechApi TangemTech API
+ * @property dispatchers coroutine dispatchers provider
+ * @property reduxStateHolder redux state holder
+ *
+[REDACTED_AUTHOR]
+ */
+class DefaultCustomTokenRepository(
+ private val tangemTechApi: TangemTechApi,
+ private val dispatchers: CoroutineDispatcherProvider,
+ private val reduxStateHolder: AppStateHolder,
+) : CustomTokenRepository {
+
+ override suspend fun findToken(address: String, networkId: String?): FoundToken {
+ val supportedTokenNetworkIds = requireNotNull(reduxStateHolder.scanResponse?.card)
+ .supportedBlockchains()
+ .filter(Blockchain::canHandleTokens)
+ .map(Blockchain::toNetworkId)
+
+ return withContext(dispatchers.io) {
+ val foundCoin = tangemTechApi.getCoins(
+ contractAddress = address,
+ networkIds = networkId ?: supportedTokenNetworkIds.joinToString(separator = ","),
+ )
+ .coins.firstNotNullOfOrNull { coin ->
+ val networksWithTheSameAddress = coin.networks.filter { network ->
+ (network.contractAddress != null || network.decimalCount != null) &&
+ network.contractAddress?.equals(address, ignoreCase = true) == true &&
+ supportedTokenNetworkIds.contains(network.networkId)
+ }
+
+ if (networksWithTheSameAddress.isNotEmpty()) {
+ coin.copy(networks = networksWithTheSameAddress)
+ } else {
+ null
+ }
+ }
+
+ foundCoin?.let(FoundTokenConverter::convert) ?: error("Token not found")
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt
new file mode 100644
index 0000000000..ebd6da9985
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/data/converters/FoundTokenConverter.kt
@@ -0,0 +1,28 @@
+package com.tangem.tap.features.customtoken.impl.data.converters
+
+import com.tangem.datasource.api.tangemTech.models.CoinsResponse
+import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
+import com.tangem.utils.converter.Converter
+
+/**
+ * Converter between data model [CoinsResponse.Coin] and domain model [FoundToken]
+ *
+[REDACTED_AUTHOR]
+ */
+object FoundTokenConverter : Converter {
+
+ override fun convert(value: CoinsResponse.Coin): FoundToken {
+ return FoundToken(
+ id = value.id,
+ name = value.name,
+ symbol = value.symbol,
+ network = value.networks.firstOrNull()?.let { network ->
+ FoundToken.Network(
+ id = network.networkId,
+ address = requireNotNull(network.contractAddress),
+ decimalCount = requireNotNull(network.decimalCount).toString(),
+ )
+ } ?: error("Found token networks is empty"),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenFeatureTogglesModule.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenFeatureTogglesModule.kt
new file mode 100644
index 0000000000..5648aa592d
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenFeatureTogglesModule.kt
@@ -0,0 +1,24 @@
+package com.tangem.tap.features.customtoken.impl.di
+
+import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
+import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
+import com.tangem.tap.features.customtoken.impl.featuretoggles.DefaultCustomTokenFeatureToggles
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Module
+@InstallIn(SingletonComponent::class)
+internal object CustomTokenFeatureTogglesModule {
+
+ @Provides
+ @Singleton
+ fun providesCustomTokenFeatureToggles(featureTogglesManager: FeatureTogglesManager): CustomTokenFeatureToggles {
+ return DefaultCustomTokenFeatureToggles(featureTogglesManager)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt
new file mode 100644
index 0000000000..236a990d22
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenInteractorModule.kt
@@ -0,0 +1,41 @@
+package com.tangem.tap.features.customtoken.impl.di
+
+import com.tangem.datasource.api.tangemTech.TangemTechApi
+import com.tangem.lib.crypto.DerivationManager
+import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository
+import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
+import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor
+import com.tangem.tap.proxy.AppStateHolder
+import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.scopes.ViewModelScoped
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Module
+@InstallIn(ViewModelComponent::class)
+internal object CustomTokenInteractorModule {
+
+ @Provides
+ @ViewModelScoped
+ fun provideCustomTokenInteractor(
+ tangemTechApi: TangemTechApi,
+ appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider,
+ reduxStateHolder: AppStateHolder,
+ derivationManager: DerivationManager,
+ ): CustomTokenInteractor {
+ return DefaultCustomTokenInteractor(
+ featureRepository = DefaultCustomTokenRepository(
+ tangemTechApi = tangemTechApi,
+ dispatchers = appCoroutineDispatcherProvider,
+ reduxStateHolder = reduxStateHolder,
+ ),
+ derivationManager = derivationManager,
+ reduxStateHolder = reduxStateHolder,
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenRouterModule.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenRouterModule.kt
new file mode 100644
index 0000000000..0c8dec0948
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/di/CustomTokenRouterModule.kt
@@ -0,0 +1,21 @@
+package com.tangem.tap.features.customtoken.impl.di
+
+import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter
+import com.tangem.tap.features.customtoken.impl.presentation.routers.DefaultCustomTokenRouter
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.scopes.ViewModelScoped
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Module
+@InstallIn(ViewModelComponent::class)
+internal object CustomTokenRouterModule {
+
+ @Provides
+ @ViewModelScoped
+ fun provideAddCustomTokenRouter(): CustomTokenRouter = DefaultCustomTokenRouter()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt
new file mode 100644
index 0000000000..1b5b0fa9cf
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenInteractor.kt
@@ -0,0 +1,19 @@
+package com.tangem.tap.features.customtoken.impl.domain
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
+import com.tangem.tap.features.wallet.models.Currency
+
+/**
+ * Custom token interactor
+ *
+[REDACTED_AUTHOR]
+ */
+interface CustomTokenInteractor {
+
+ /** Find token by [address] and [blockchain] */
+ suspend fun findToken(address: String, blockchain: Blockchain): FoundToken
+
+ /** Save token [currency] with contact address [address] */
+ suspend fun saveToken(currency: Currency, address: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt
new file mode 100644
index 0000000000..13b5d1c043
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/CustomTokenRepository.kt
@@ -0,0 +1,14 @@
+package com.tangem.tap.features.customtoken.impl.domain
+
+import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
+
+/**
+ * Custom token repository
+ *
+[REDACTED_AUTHOR]
+ */
+interface CustomTokenRepository {
+
+ /** Find token by [address] and [networkId] */
+ suspend fun findToken(address: String, networkId: String?): FoundToken
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt
new file mode 100644
index 0000000000..6ecbab2382
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/DefaultCustomTokenInteractor.kt
@@ -0,0 +1,96 @@
+package com.tangem.tap.features.customtoken.impl.domain
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.common.extensions.guard
+import com.tangem.common.flatMap
+import com.tangem.domain.common.extensions.toNetworkId
+import com.tangem.domain.models.scan.ScanResponse
+import com.tangem.lib.crypto.DerivationManager
+import com.tangem.lib.crypto.models.Currency.NativeToken
+import com.tangem.lib.crypto.models.Currency.NonNativeToken
+import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
+import com.tangem.tap.features.wallet.models.Currency
+import com.tangem.tap.proxy.AppStateHolder
+import com.tangem.tap.scope
+import com.tangem.tap.userWalletsListManager
+import com.tangem.tap.walletCurrenciesManager
+import kotlinx.coroutines.launch
+import timber.log.Timber
+
+/**
+ * Default implementation of custom token interactor
+ *
+ * @property featureRepository feature repository
+ * @property derivationManager derivation manager
+ * @property reduxStateHolder redux state holder
+ *
+[REDACTED_AUTHOR]
+ */
+class DefaultCustomTokenInteractor(
+ private val featureRepository: CustomTokenRepository,
+ private val derivationManager: DerivationManager,
+ private val reduxStateHolder: AppStateHolder,
+) : CustomTokenInteractor {
+
+ override suspend fun findToken(address: String, blockchain: Blockchain): FoundToken {
+ return featureRepository.findToken(
+ address = address,
+ networkId = if (blockchain != Blockchain.Unknown) blockchain.toNetworkId() else null,
+ )
+ }
+
+ override suspend fun saveToken(currency: Currency, address: String) {
+ val hasDerivation = derivationManager.hasDerivation(
+ networkId = currency.blockchain.toNetworkId(),
+ derivationPath = requireNotNull(currency.derivationPath),
+ )
+
+ if (!hasDerivation) {
+ derivationManager.deriveMissingBlockchains(
+ when (currency) {
+ is Currency.Blockchain -> NativeToken(
+ id = requireNotNull(currency.coinId),
+ name = currency.currencyName,
+ symbol = currency.currencySymbol,
+ networkId = currency.blockchain.toNetworkId(),
+ )
+
+ is Currency.Token -> NonNativeToken(
+ id = requireNotNull(currency.coinId),
+ name = currency.currencyName,
+ symbol = currency.currencySymbol,
+ networkId = currency.blockchain.toNetworkId(),
+ contractAddress = address,
+ decimalCount = currency.decimals,
+ )
+ },
+ )
+
+ submitAdd(
+ scanResponse = requireNotNull(reduxStateHolder.scanResponse),
+ currency = currency,
+ )
+ }
+ }
+
+ private fun submitAdd(scanResponse: ScanResponse, currency: Currency) {
+ val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
+ Timber.e("Unable to add currencies, no user wallet selected")
+ return
+ }
+ scope.launch {
+ userWalletsListManager.update(
+ userWalletId = selectedUserWallet.walletId,
+ update = { userWallet ->
+ userWallet.copy(scanResponse = scanResponse)
+ },
+ )
+ .flatMap { updatedUserWallet ->
+ walletCurrenciesManager.addCurrencies(
+ userWallet = updatedUserWallet,
+ currenciesToAdd = listOf(currency),
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt
new file mode 100644
index 0000000000..25c3cb5836
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/domain/models/FoundToken.kt
@@ -0,0 +1,23 @@
+package com.tangem.tap.features.customtoken.impl.domain.models
+
+/**
+ * Found token model
+ *
+ * @property id id
+ * @property name name
+ * @property symbol symbol
+ * @property network network
+ *
+[REDACTED_AUTHOR]
+ */
+data class FoundToken(val id: String, val name: String, val symbol: String, val network: Network) {
+
+ /**
+ * Found token network
+ *
+ * @property id id
+ * @property address address
+ * @property decimalCount decimal count
+ */
+ data class Network(val id: String, val address: String, val decimalCount: String)
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt
new file mode 100644
index 0000000000..fbdc41b2c0
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/featuretoggles/DefaultCustomTokenFeatureToggles.kt
@@ -0,0 +1,19 @@
+package com.tangem.tap.features.customtoken.impl.featuretoggles
+
+import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
+import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
+
+/**
+ * Default implementation of CustomToken feature toggles
+ *
+ * @property featureTogglesManager manager for getting information about the availability of feature toggles
+ *
+[REDACTED_AUTHOR]
+ */
+internal class DefaultCustomTokenFeatureToggles(
+ private val featureTogglesManager: FeatureTogglesManager,
+) : CustomTokenFeatureToggles {
+
+ override val isRedesignedScreenEnabled: Boolean
+ get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED")
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt
new file mode 100644
index 0000000000..ee63fc97e0
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/AddCustomTokenFragment.kt
@@ -0,0 +1,46 @@
+package com.tangem.tap.features.customtoken.impl.presentation
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.fragment.app.Fragment
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.transition.TransitionInflater
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen
+import com.tangem.tap.features.customtoken.impl.presentation.viewmodels.AddCustomTokenViewModel
+import com.tangem.wallet.R
+import dagger.hilt.android.AndroidEntryPoint
+
+/**
+ * Add custom token screen
+ *
+[REDACTED_AUTHOR]
+ */
+@AndroidEntryPoint
+internal class AddCustomTokenFragment : Fragment() {
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ with(TransitionInflater.from(requireContext())) {
+ enterTransition = inflateTransition(R.transition.fade)
+ exitTransition = inflateTransition(R.transition.fade)
+ }
+
+ return ComposeView(inflater.context).apply {
+ setContent {
+ isTransitionGroup = true
+
+ val viewModel = hiltViewModel().apply {
+ LocalLifecycleOwner.current.lifecycle.addObserver(this)
+ }
+
+ TangemTheme {
+ AddCustomTokenScreen(stateHolder = viewModel.uiState)
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt
new file mode 100644
index 0000000000..90dcb8a583
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/models/AddCustomTokenViewsModels.kt
@@ -0,0 +1,279 @@
+package com.tangem.tap.features.customtoken.impl.presentation.models
+
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardType
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.tap.features.details.ui.cardsettings.TextReference
+import com.tangem.wallet.R
+
+/**
+ * Toolbar model of add custom token screen
+ *
+ * @property title title
+ * @property onBackButtonClick lambda be invoked when back button is been pressed
+ */
+internal data class AddCustomTokensToolbar(val title: TextReference, val onBackButtonClick: () -> Unit)
+
+/**
+ * Model of block with fields for testing
+ *
+ * @property chooseTokenButtonText choose token button text
+ * @property clearButtonText clear button text
+ * @property resetButtonText reset button text
+ * @property onClearAddressButtonClick lambda be invoked when clear address button is been pressed
+ * @property onResetButtonClick lambda be invoked when reset form fields button is been pressed
+ */
+internal data class AddCustomTokenTestBlock(
+ val chooseTokenButtonText: String,
+ val clearButtonText: String,
+ val resetButtonText: String,
+ val onClearAddressButtonClick: () -> Unit,
+ val onResetButtonClick: () -> Unit,
+)
+
+/**
+ * Bottom sheet model for choose custom token
+ *
+ * @property categoriesBlocks tokens categories
+ * @property onTestTokenClick lambda be invoked when token is been pressed
+ */
+internal data class AddCustomTokenChooseTokenBottomSheet(
+ val categoriesBlocks: List,
+ val onTestTokenClick: (String) -> Unit,
+) {
+
+ /**
+ * Tokens category model
+ *
+ * @property name category name
+ * @property items category items
+ */
+ data class TokensCategoryBlock(val name: String, val items: List)
+
+ /**
+ * Test token model
+ *
+ * @property name token name
+ * @property address token address
+ */
+ data class TestTokenItem(val name: String, val address: String)
+}
+
+/**
+ * Form with fields model of add custom token screen
+ *
+ * @property contractAddressInputField input field to enter the contract address
+ * @property networkSelectorField selector field to select the token network
+ * @property tokenNameInputField input field to enter the token name
+ * @property tokenSymbolInputField input field to enter the token symbol
+ * @property decimalsInputField input field to enter the token decimals
+ * @property derivationPathSelectorField selector field to select the derivation path
+ */
+internal data class AddCustomTokenForm(
+ val contractAddressInputField: AddCustomTokenInputField.ContactAddress,
+ val networkSelectorField: AddCustomTokenSelectorField.Network,
+ val tokenNameInputField: AddCustomTokenInputField.TokenName,
+ val tokenSymbolInputField: AddCustomTokenInputField.TokenSymbol,
+ val decimalsInputField: AddCustomTokenInputField.Decimals,
+ val derivationPathSelectorField: AddCustomTokenSelectorField.DerivationPath?,
+)
+
+/** Base input field model of add custom token screen */
+internal sealed interface AddCustomTokenInputField {
+
+ /** Current value */
+ val value: String
+
+ /** Lambda be invoked when value is been changed */
+ val onValueChange: (String) -> Unit
+
+ /** Keyboard options */
+ val keyboardOptions: KeyboardOptions
+
+ /** Label */
+ val label: TextReference
+
+ /** Input availability */
+ val isEnabled: Boolean
+
+ /** Flag that determine if current value has error */
+ val isError: Boolean
+
+ /** Placeholder (hint) */
+ val placeholder: TextReference
+
+ /** Flag that determine the processing of current value */
+ val isLoading: Boolean
+
+ /**
+ * Input field model to enter the contract address
+ *
+ * @property value current value
+ * @property onValueChange lambda be invoked when value is been changed
+ * @property isError flag that determine if current value has error
+ * @property isLoading flag that determine the processing of current value
+ */
+ data class ContactAddress(
+ override val value: String,
+ override val onValueChange: (String) -> Unit,
+ override val isError: Boolean,
+ override val isLoading: Boolean,
+ ) : AddCustomTokenInputField {
+ override val isEnabled = true
+ override val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
+ override val label = TextReference.Res(R.string.custom_token_contract_address_input_title)
+ override val placeholder = TextReference.Str(value = "0x0000000000000000000000000000000000000000")
+ }
+
+ /**
+ * Input field model to enter the token name
+ *
+ * @property value current value
+ * @property onValueChange lambda be invoked when value is been changed
+ * @property isEnabled input availability
+ * @property isError flag that determine if current value has error
+ */
+ data class TokenName(
+ override val value: String,
+ override val onValueChange: (String) -> Unit,
+ override val isEnabled: Boolean,
+ override val isError: Boolean,
+ ) : AddCustomTokenInputField {
+ override val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
+ override val label = TextReference.Res(R.string.custom_token_name_input_title)
+ override val placeholder = TextReference.Res(id = R.string.custom_token_name_input_placeholder)
+ override val isLoading = false
+ }
+
+ /**
+ * Input field model to enter the token symbol
+ *
+ * @property value current value
+ * @property onValueChange lambda be invoked when value is been changed
+ * @property isEnabled input availability
+ * @property isError flag that determine if current value has error
+ */
+ data class TokenSymbol(
+ override val value: String,
+ override val onValueChange: (String) -> Unit,
+ override val isEnabled: Boolean,
+ override val isError: Boolean,
+ ) : AddCustomTokenInputField {
+ override val keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next)
+ override val label = TextReference.Res(R.string.custom_token_token_symbol_input_title)
+ override val placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder)
+ override val isLoading = false
+ }
+
+ /**
+ * Input field model to enter the token decimals
+ *
+ * @property value current value
+ * @property onValueChange lambda be invoked when value is been changed
+ * @property isEnabled input availability
+ * @property isError flag that determine if current value has error
+ */
+ data class Decimals(
+ override val value: String,
+ override val onValueChange: (String) -> Unit,
+ override val isEnabled: Boolean,
+ override val isError: Boolean,
+ ) : AddCustomTokenInputField {
+ override val keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next)
+ override val label = TextReference.Res(R.string.custom_token_decimals_input_title)
+ override val placeholder = TextReference.Str(value = "8")
+ override val isLoading = false
+ }
+}
+
+/** Base selector field model of add custom token screen */
+internal sealed interface AddCustomTokenSelectorField {
+
+ /** Selection availability */
+ val isEnabled: Boolean
+
+ /** Label string resource id */
+ val label: TextReference
+
+ /** Selected menu item */
+ val selectedItem: SelectorItem
+
+ /** Menu items */
+ val items: List
+
+ /** Lambda be invoked when menu item is been selected */
+ val onMenuItemClick: (Int) -> Unit
+
+ /**
+ * Network selector model
+ *
+ * @property selectedItem selected menu item
+ * @property items menu items
+ * @property onMenuItemClick lambda be invoked when menu item is been selected
+ */
+ data class Network(
+ override val selectedItem: SelectorItem.Title,
+ override val items: List,
+ override val onMenuItemClick: (Int) -> Unit,
+ ) : AddCustomTokenSelectorField {
+ override val isEnabled = true
+ override val label = TextReference.Res(R.string.custom_token_network_input_title)
+ }
+
+ /**
+ * Derivation path selector model
+ *
+ * @property isEnabled selection availability
+ * @property selectedItem selected menu item
+ * @property items menu items
+ * @property onMenuItemClick lambda be invoked when menu item is been selected
+ */
+ data class DerivationPath(
+ override val isEnabled: Boolean,
+ override val selectedItem: SelectorItem.TitleWithSubtitle,
+ override val items: List,
+ override val onMenuItemClick: (Int) -> Unit,
+ ) : AddCustomTokenSelectorField {
+ override val label = TextReference.Res(R.string.custom_token_derivation_path_input_title)
+ }
+
+ /** Base menu item model */
+ sealed interface SelectorItem {
+
+ /** Title */
+ val title: TextReference
+
+ /** Blockchain */
+ val blockchain: Blockchain
+
+ /**
+ * Menu item with title
+ *
+ * @property title title text
+ * @property blockchain blockchain
+ */
+ data class Title(override val title: TextReference, override val blockchain: Blockchain) : SelectorItem
+
+ /**
+ * Menu item with title ans subtitle
+ *
+ * @property title title text
+ * @property subtitle subtitle text
+ * @property blockchain blockchain
+ */
+ data class TitleWithSubtitle(
+ override val title: TextReference,
+ val subtitle: TextReference,
+ override val blockchain: Blockchain,
+ ) : SelectorItem
+ }
+}
+
+/**
+ * Floating button of add custom token screen
+ *
+ * @property isEnabled button availability
+ * @property onClick lambda be invoked when button is been pressed
+ */
+internal data class AddCustomTokenFloatingButton(val isEnabled: Boolean, val onClick: () -> Unit)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt
new file mode 100644
index 0000000000..933bda4bc7
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/CustomTokenRouter.kt
@@ -0,0 +1,12 @@
+package com.tangem.tap.features.customtoken.impl.presentation.routers
+
+/**
+ * Custom token feature router
+ *
+[REDACTED_AUTHOR]
+ */
+internal interface CustomTokenRouter {
+
+ /** Return to last screen */
+ fun popBackStack()
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt
new file mode 100644
index 0000000000..9460879f92
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/routers/DefaultCustomTokenRouter.kt
@@ -0,0 +1,12 @@
+package com.tangem.tap.features.customtoken.impl.presentation.routers
+
+import com.tangem.tap.common.redux.navigation.NavigationAction
+import com.tangem.tap.store
+
+/** Default implementation of custom token feature router */
+internal class DefaultCustomTokenRouter : CustomTokenRouter {
+
+ override fun popBackStack() {
+ store.dispatch(NavigationAction.PopBackTo())
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/states/AddCustomTokenStateHolder.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/states/AddCustomTokenStateHolder.kt
new file mode 100644
index 0000000000..17b0f92c9c
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/states/AddCustomTokenStateHolder.kt
@@ -0,0 +1,91 @@
+package com.tangem.tap.features.customtoken.impl.presentation.states
+
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
+import com.tangem.tap.features.details.ui.cardsettings.TextReference
+
+/**
+ * State holder of add custom token screen
+ *
+[REDACTED_AUTHOR]
+ */
+internal sealed interface AddCustomTokenStateHolder {
+
+ /** Lambda be invoked when system back action is been called */
+ val onBackButtonClick: () -> Unit
+
+ /** Toolbar model */
+ val toolbar: AddCustomTokensToolbar
+
+ /** Form model */
+ val form: AddCustomTokenForm
+
+ /** Warnings */
+ val warnings: List
+
+ /** Floating button model */
+ val floatingButton: AddCustomTokenFloatingButton
+
+ /**
+ * Util function that allow to make a copy
+ *
+ * @param onBackButtonClick lambda be invoked when system back action is been called
+ * @param toolbar toolbar model
+ * @param form form model
+ * @param warnings warnings
+ * @param floatingButton floating button model
+ */
+ fun copySealed(
+ onBackButtonClick: () -> Unit = this.onBackButtonClick,
+ toolbar: AddCustomTokensToolbar = this.toolbar,
+ form: AddCustomTokenForm = this.form,
+ warnings: List = this.warnings,
+ floatingButton: AddCustomTokenFloatingButton = this.floatingButton,
+ ): AddCustomTokenStateHolder {
+ return when (this) {
+ is Content -> copy(onBackButtonClick, toolbar, form, warnings, floatingButton)
+ is TestContent -> copy(onBackButtonClick, toolbar, form, warnings, floatingButton)
+ }
+ }
+
+ /**
+ * Content state
+ *
+ * @property onBackButtonClick lambda be invoked when system back action is been called
+ * @property toolbar toolbar model
+ * @property form form model
+ * @property warnings warnings
+ * @property floatingButton floating button model
+ */
+ data class Content(
+ override val onBackButtonClick: () -> Unit,
+ override val toolbar: AddCustomTokensToolbar,
+ override val form: AddCustomTokenForm,
+ override val warnings: List,
+ override val floatingButton: AddCustomTokenFloatingButton,
+ ) : AddCustomTokenStateHolder
+
+ /**
+ * Content state with fields for testing
+ *
+ * @property onBackButtonClick lambda be invoked when system back action is been called
+ * @property toolbar toolbar model
+ * @property form form model
+ * @property warnings warnings
+ * @property floatingButton floating button model
+ * @property testBlock test block model
+ * @property bottomSheet bottom sheet model
+ */
+ data class TestContent(
+ override val onBackButtonClick: () -> Unit,
+ override val toolbar: AddCustomTokensToolbar,
+ override val form: AddCustomTokenForm,
+ override val warnings: List,
+ override val floatingButton: AddCustomTokenFloatingButton,
+ val testBlock: AddCustomTokenTestBlock,
+ val bottomSheet: AddCustomTokenChooseTokenBottomSheet,
+ ) : AddCustomTokenStateHolder
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt
new file mode 100644
index 0000000000..7b746c3ec4
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenContent.kt
@@ -0,0 +1,121 @@
+package com.tangem.tap.features.customtoken.impl.presentation.ui
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.FabPosition
+import androidx.compose.material.Scaffold
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Preview
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
+import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
+import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton
+import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm
+import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar
+import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarning
+import com.tangem.tap.features.details.ui.cardsettings.TextReference
+import com.tangem.wallet.R
+
+/**
+ * Add custom token content
+ *
+ * @param state screen state
+ *
+[REDACTED_AUTHOR]
+ */
+@Composable
+internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content) {
+ BackHandler(onBack = state.onBackButtonClick)
+
+ Scaffold(
+ topBar = {
+ AddCustomTokenToolbar(
+ title = state.toolbar.title,
+ onBackButtonClick = state.toolbar.onBackButtonClick,
+ )
+ },
+ floatingActionButton = { AddCustomTokenFloatingButton(model = state.floatingButton) },
+ floatingActionButtonPosition = FabPosition.Center,
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(paddingValues = it)
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState()),
+ ) {
+ AddCustomTokenForm(model = state.form)
+
+ state.warnings.forEach { description ->
+ key(description) {
+ AddCustomTokenWarning(description)
+ }
+ }
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Composable
+private fun Preview_AddCustomTokenContent() {
+ TangemTheme {
+ AddCustomTokenContent(
+ state = AddCustomTokenStateHolder.Content(
+ onBackButtonClick = {},
+ toolbar = AddCustomTokensToolbar(
+ title = TextReference.Res(R.string.add_custom_token_title),
+ onBackButtonClick = {},
+ ),
+ form = com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm(
+ contractAddressInputField = AddCustomTokenInputField.ContactAddress(
+ value = "",
+ onValueChange = {},
+ isError = false,
+ isLoading = false,
+ ),
+ networkSelectorField = AddCustomTokenSelectorField.Network(
+ selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
+ title = TextReference.Str("Avalanche"),
+ blockchain = Blockchain.Avalanche,
+ ),
+ items = listOf(),
+ onMenuItemClick = {},
+ ),
+ tokenNameInputField = AddCustomTokenInputField.TokenName(
+ value = "",
+ onValueChange = {},
+ isEnabled = false,
+ isError = false,
+ ),
+ tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
+ value = "",
+ onValueChange = {},
+ isEnabled = false,
+ isError = false,
+ ),
+ decimalsInputField = AddCustomTokenInputField.Decimals(
+ value = "",
+ onValueChange = {},
+ isEnabled = false,
+ isError = false,
+ ),
+ derivationPathSelectorField = null,
+ ),
+ warnings = listOf(),
+ floatingButton = AddCustomTokenFloatingButton(
+ isEnabled = false,
+ onClick = {},
+ ),
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt
new file mode 100644
index 0000000000..82f284fb98
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenScreen.kt
@@ -0,0 +1,20 @@
+package com.tangem.tap.features.customtoken.impl.presentation.ui
+
+import androidx.compose.runtime.Composable
+import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
+
+/**
+ * Add custom token screen
+ *
+ * @param stateHolder state holder
+ *
+[REDACTED_AUTHOR]
+ */
+@Suppress("UnusedPrivateMember")
+@Composable
+internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder) {
+ when (stateHolder) {
+ is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(state = stateHolder)
+ is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(state = stateHolder)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt
new file mode 100644
index 0000000000..dce924a655
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/AddCustomTokenTestContent.kt
@@ -0,0 +1,311 @@
+package com.tangem.tap.features.customtoken.impl.presentation.ui
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.BottomSheetScaffold
+import androidx.compose.material.BottomSheetScaffoldState
+import androidx.compose.material.BottomSheetState
+import androidx.compose.material.BottomSheetValue
+import androidx.compose.material.Divider
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.FabPosition
+import androidx.compose.material.Text
+import androidx.compose.material.rememberBottomSheetScaffoldState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.core.ui.components.PrimaryButton
+import com.tangem.core.ui.components.SpacerH8
+import com.tangem.core.ui.components.atoms.Hand
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
+import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
+import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenFloatingButton
+import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenForm
+import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenToolbar
+import com.tangem.tap.features.details.ui.cardsettings.TextReference
+import com.tangem.wallet.R
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Add custom token content for testing
+ *
+ * @param state screen state
+ *
+[REDACTED_AUTHOR]
+ */
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent) {
+ val coroutineScope = rememberCoroutineScope()
+ val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
+ bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed),
+ )
+
+ BackHandler(
+ onBack = {
+ onBackButtonClicked(
+ coroutineScope = coroutineScope,
+ bottomSheetScaffoldState = bottomSheetScaffoldState,
+ defaultAction = state.onBackButtonClick,
+ )
+ },
+ )
+
+ BottomSheetScaffold(
+ sheetContent = {
+ SheetContent(
+ coroutineScope = coroutineScope,
+ bottomSheetScaffoldState = bottomSheetScaffoldState,
+ model = state.bottomSheet,
+ )
+ },
+ scaffoldState = bottomSheetScaffoldState,
+ topBar = {
+ AddCustomTokenToolbar(
+ title = state.toolbar.title,
+ onBackButtonClick = {
+ onBackButtonClicked(
+ coroutineScope,
+ bottomSheetScaffoldState,
+ defaultAction = state.toolbar.onBackButtonClick,
+ )
+ },
+ )
+ },
+ floatingActionButton = { AddCustomTokenFloatingButton(model = state.floatingButton) },
+ floatingActionButtonPosition = FabPosition.Center,
+ sheetBackgroundColor = TangemTheme.colors.background.secondary,
+ sheetPeekHeight = TangemTheme.dimens.size0,
+ backgroundColor = TangemTheme.colors.background.secondary,
+ ) {
+ Column(
+ modifier = Modifier
+ .verticalScroll(rememberScrollState())
+ .padding(it),
+ ) {
+ TestBlock(
+ model = state.testBlock,
+ coroutineScope,
+ bottomSheetScaffoldState,
+ )
+
+ AddCustomTokenForm(model = state.form)
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+private fun onBackButtonClicked(
+ coroutineScope: CoroutineScope,
+ bottomSheetScaffoldState: BottomSheetScaffoldState,
+ defaultAction: () -> Unit,
+) {
+ coroutineScope.launch {
+ if (bottomSheetScaffoldState.bottomSheetState.isExpanded) {
+ bottomSheetScaffoldState.bottomSheetState.collapse()
+ } else {
+ defaultAction()
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun SheetContent(
+ model: AddCustomTokenChooseTokenBottomSheet,
+ coroutineScope: CoroutineScope,
+ bottomSheetScaffoldState: BottomSheetScaffoldState,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(LocalConfiguration.current.screenHeightDp.dp - TangemTheme.dimens.spacing16),
+ ) {
+ Hand()
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState()),
+ ) {
+ model.categoriesBlocks.forEachIndexed { index, categoryBlock ->
+ key(categoryBlock) {
+ Column {
+ TokensList(
+ title = categoryBlock.name,
+ tokens = categoryBlock.items,
+ onTestTokenClick = { address ->
+ model.onTestTokenClick(address)
+ coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.collapse() }
+ },
+ )
+
+ if (model.categoriesBlocks.lastIndex != index) {
+ Divider()
+ SpacerH8()
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun TokensList(title: String, tokens: List, onTestTokenClick: (String) -> Unit) {
+ Text(
+ text = title,
+ modifier = Modifier.padding(
+ horizontal = TangemTheme.dimens.spacing24,
+ vertical = TangemTheme.dimens.spacing8,
+ ),
+ maxLines = 1,
+ style = TangemTheme.typography.h3,
+ )
+
+ tokens.forEach { token ->
+ key(token) {
+ PrimaryButton(
+ text = token.name,
+ onClick = { onTestTokenClick(token.address) },
+ modifier = Modifier
+ .padding(horizontal = TangemTheme.dimens.spacing16)
+ .padding(bottom = TangemTheme.dimens.spacing8)
+ .fillMaxWidth(),
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
+@Composable
+private fun TestBlock(
+ model: AddCustomTokenTestBlock,
+ coroutineScope: CoroutineScope,
+ bottomSheetScaffoldState: BottomSheetScaffoldState,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = TangemTheme.dimens.spacing16)
+ .padding(top = TangemTheme.dimens.spacing16),
+ verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
+ ) {
+ val softwareKeyboardController = LocalSoftwareKeyboardController.current
+ PrimaryButton(
+ text = model.chooseTokenButtonText,
+ onClick = {
+ softwareKeyboardController?.hide()
+ coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.expand() }
+ },
+ modifier = Modifier.fillMaxWidth(),
+ )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ PrimaryButton(
+ text = model.clearButtonText,
+ onClick = model.onClearAddressButtonClick,
+ modifier = Modifier.weight(1f),
+ )
+ PrimaryButton(
+ text = model.resetButtonText,
+ onClick = model.onResetButtonClick,
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+}
+
+@Preview(showSystemUi = true)
+@Composable
+private fun Preview_AddCustomTokenTestContent() {
+ TangemTheme {
+ AddCustomTokenTestContent(
+ state = AddCustomTokenStateHolder.TestContent(
+ onBackButtonClick = {},
+ toolbar = AddCustomTokensToolbar(
+ title = TextReference.Res(R.string.add_custom_token_title),
+ onBackButtonClick = {},
+ ),
+ form = com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm(
+ contractAddressInputField = AddCustomTokenInputField.ContactAddress(
+ value = "",
+ onValueChange = {},
+ isError = false,
+ isLoading = false,
+ ),
+ networkSelectorField = AddCustomTokenSelectorField.Network(
+ selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
+ title = TextReference.Str(value = "Avalanche"),
+ blockchain = Blockchain.Avalanche,
+ ),
+ items = listOf(),
+ onMenuItemClick = {},
+ ),
+ tokenNameInputField = AddCustomTokenInputField.TokenName(
+ value = "",
+ onValueChange = {},
+ isEnabled = false,
+ isError = false,
+ ),
+ tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
+ value = "",
+ onValueChange = {},
+ isEnabled = false,
+ isError = false,
+ ),
+ decimalsInputField = AddCustomTokenInputField.Decimals(
+ value = "",
+ onValueChange = {},
+ isEnabled = false,
+ isError = false,
+ ),
+ derivationPathSelectorField = null,
+ ),
+ warnings = listOf(),
+ floatingButton = AddCustomTokenFloatingButton(
+ isEnabled = false,
+ onClick = {},
+ ),
+ testBlock = AddCustomTokenTestBlock(
+ chooseTokenButtonText = "Choose token",
+ clearButtonText = "Clear address",
+ resetButtonText = "Reset",
+ onClearAddressButtonClick = {},
+ onResetButtonClick = {},
+ ),
+ bottomSheet = AddCustomTokenChooseTokenBottomSheet(
+ categoriesBlocks = listOf(),
+ onTestTokenClick = {},
+ ),
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt
new file mode 100644
index 0000000000..082bfe4939
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenFloatingButton.kt
@@ -0,0 +1,51 @@
+package com.tangem.tap.features.customtoken.impl.presentation.ui.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.imePadding
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.tangem.core.ui.components.PrimaryButtonIconLeft
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
+import com.tangem.wallet.R
+
+/**
+ * Add custom token floating button. Attached above the keyboard.
+ *
+ * @param model button model
+ *
+[REDACTED_AUTHOR]
+ */
+@Composable
+internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton) {
+ PrimaryButtonIconLeft(
+ modifier = Modifier
+ .imePadding()
+ .padding(horizontal = TangemTheme.dimens.spacing16)
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.common_add),
+ icon = painterResource(id = R.drawable.ic_plus_24),
+ enabled = model.isEnabled,
+ onClick = model.onClick,
+ )
+}
+
+@Preview
+@Composable
+private fun Preview_AddCustomTokenFloatingButton_Enabled() {
+ TangemTheme {
+ AddCustomTokenFloatingButton(model = AddCustomTokenFloatingButton(isEnabled = true, onClick = {}))
+ }
+}
+
+@Preview
+@Composable
+private fun Preview_AddCustomTokenFloatingButton_Disabled() {
+ TangemTheme {
+ AddCustomTokenFloatingButton(model = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}))
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt
new file mode 100644
index 0000000000..44904ac319
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenForm.kt
@@ -0,0 +1,216 @@
+package com.tangem.tap.features.customtoken.impl.presentation.ui.components
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.DropdownMenuItem
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ExposedDropdownMenuBox
+import androidx.compose.material.ExposedDropdownMenuDefaults
+import androidx.compose.material.LinearProgressIndicator
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.tooling.preview.Preview
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.common.compose.TangemTextFieldsDefault
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
+import com.tangem.tap.features.details.ui.cardsettings.TextReference
+import com.tangem.tap.features.details.ui.cardsettings.resolveReference
+
+/**
+ * Add custom token form
+ *
+ * @param model component model
+ *
+[REDACTED_AUTHOR]
+ */
+@Composable
+internal fun AddCustomTokenForm(model: AddCustomTokenForm) {
+ Card(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(TangemTheme.dimens.spacing16),
+ shape = RoundedCornerShape(TangemTheme.dimens.radius8),
+ backgroundColor = TangemTheme.colors.background.primary,
+ elevation = TangemTheme.dimens.elevation4,
+ ) {
+ Column(
+ modifier = Modifier.padding(TangemTheme.dimens.spacing16),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
+ ) {
+ InputField(model = model.contractAddressInputField)
+ SelectorField(model = model.networkSelectorField)
+ InputField(model = model.tokenNameInputField)
+ InputField(model = model.tokenSymbolInputField)
+ InputField(model = model.decimalsInputField)
+ model.derivationPathSelectorField?.let { SelectorField(model = it) }
+ }
+ }
+}
+
+@Composable
+private fun InputField(model: AddCustomTokenInputField) {
+ Box {
+ OutlinedTextField(
+ modifier = Modifier.fillMaxWidth(),
+ value = model.value,
+ onValueChange = model.onValueChange,
+ keyboardOptions = model.keyboardOptions,
+ label = {
+ Text(
+ text = model.label.resolveReference(),
+ style = TangemTheme.typography.caption,
+ color = TangemTextFieldsDefault.defaultTextFieldColors.labelColor(
+ enabled = model.isEnabled,
+ error = model.isError,
+ interactionSource = remember { MutableInteractionSource() },
+ ).value,
+ )
+ },
+ placeholder = {
+ Text(
+ text = model.placeholder.resolveReference(),
+ style = TangemTheme.typography.body1,
+ color = TangemTextFieldsDefault.defaultTextFieldColors
+ .placeholderColor(enabled = model.isEnabled)
+ .value,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ },
+ singleLine = true,
+ enabled = model.isEnabled,
+ isError = model.isError,
+ colors = TangemTextFieldsDefault.defaultTextFieldColors,
+ )
+
+ AnimatedVisibility(
+ visible = model.isLoading,
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.BottomCenter)
+ .padding(horizontal = TangemTheme.dimens.spacing6)
+ .padding(bottom = TangemTheme.dimens.spacing6),
+ ) {
+ LinearProgressIndicator(color = TangemTheme.colors.icon.primary1)
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun SelectorField(model: AddCustomTokenSelectorField) {
+ var isExpanded by remember { mutableStateOf(value = false) }
+
+ ExposedDropdownMenuBox(
+ expanded = isExpanded,
+ onExpandedChange = { isExpanded = !isExpanded },
+ ) {
+ OutlinedTextField(
+ value = when (val item = model.selectedItem) {
+ is AddCustomTokenSelectorField.SelectorItem.Title -> item.title
+ is AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle -> item.subtitle
+ }.resolveReference(),
+ modifier = Modifier.fillMaxWidth(),
+ onValueChange = {},
+ readOnly = true,
+ enabled = model.isEnabled,
+ label = { Text(text = model.label.resolveReference()) },
+ trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
+ colors = TangemTextFieldsDefault.defaultTextFieldColors,
+ )
+
+ ExposedDropdownMenu(
+ expanded = isExpanded && model.isEnabled,
+ onDismissRequest = { isExpanded = false },
+ ) {
+ FocusRequester
+ model.items.forEachIndexed { index, item ->
+ DropdownMenuItem(
+ onClick = {
+ model.onMenuItemClick(index)
+ isExpanded = false
+ },
+ ) {
+ Column {
+ Text(text = item.title.resolveReference())
+
+ val subtitle = (item as? AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle)
+ ?.subtitle?.resolveReference()
+
+ if (!subtitle.isNullOrBlank()) {
+ Text(
+ text = subtitle,
+ color = TangemTheme.colors.text.secondary,
+ maxLines = 1,
+ style = TangemTheme.typography.caption,
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Preview
+@Composable
+private fun Preview_AddCustomTokenForm() {
+ TangemTheme {
+ AddCustomTokenForm(
+ AddCustomTokenForm(
+ contractAddressInputField = AddCustomTokenInputField.ContactAddress(
+ value = "",
+ onValueChange = {},
+ isError = false,
+ isLoading = false,
+ ),
+ networkSelectorField = AddCustomTokenSelectorField.Network(
+ selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
+ title = TextReference.Str(value = "Avalanche"),
+ blockchain = Blockchain.Avalanche,
+ ),
+ items = listOf(),
+ onMenuItemClick = {},
+ ),
+ tokenNameInputField = AddCustomTokenInputField.TokenName(
+ value = "",
+ onValueChange = {},
+ isEnabled = false,
+ isError = false,
+ ),
+ tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
+ value = "",
+ onValueChange = {},
+ isEnabled = false,
+ isError = false,
+ ),
+ decimalsInputField = AddCustomTokenInputField.Decimals(
+ value = "",
+ onValueChange = {},
+ isEnabled = false,
+ isError = false,
+ ),
+ derivationPathSelectorField = null,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt
new file mode 100644
index 0000000000..8587b990bf
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenToolbar.kt
@@ -0,0 +1,54 @@
+package com.tangem.tap.features.customtoken.impl.presentation.ui.components
+
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.width
+import androidx.compose.material.Icon
+import androidx.compose.material.IconButton
+import androidx.compose.material.Text
+import androidx.compose.material.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.tangem.core.ui.R
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.features.details.ui.cardsettings.TextReference
+import com.tangem.tap.features.details.ui.cardsettings.resolveReference
+
+/**
+ * Add custom token toolbar
+ *
+ * @param title title
+ * @param onBackButtonClick lambda be invoked when BackButton is been pressed
+ *
+[REDACTED_AUTHOR]
+ */
+@Composable
+internal fun AddCustomTokenToolbar(title: TextReference, onBackButtonClick: () -> Unit) {
+ TopAppBar(backgroundColor = TangemTheme.colors.background.secondary) {
+ IconButton(onClick = onBackButtonClick) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_back_24),
+ contentDescription = null,
+ tint = TangemTheme.colors.icon.secondary,
+ )
+ }
+
+ Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing26))
+
+ Text(
+ text = title.resolveReference(),
+ color = TangemTheme.colors.text.primary1,
+ maxLines = 1,
+ style = TangemTheme.typography.h3,
+ )
+ }
+}
+
+@Preview
+@Composable
+internal fun Preview_AddCustomTokenToolbar() {
+ TangemTheme {
+ AddCustomTokenToolbar(title = TextReference.Res(R.string.add_custom_token_title), onBackButtonClick = {})
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarning.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarning.kt
new file mode 100644
index 0000000000..5eb76c6e66
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/ui/components/AddCustomTokenWarning.kt
@@ -0,0 +1,54 @@
+package com.tangem.tap.features.customtoken.impl.presentation.ui.components
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Card
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.unit.sp
+import com.tangem.core.ui.res.TangemColorPalette
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.features.details.ui.cardsettings.TextReference
+import com.tangem.tap.features.details.ui.cardsettings.resolveReference
+import com.tangem.wallet.R
+
+/**
+ * Add custom token warning component
+ * FIXME("Incorrect typography. Replace with typography from design system")
+ *
+ * @param description warning description
+ * @param modifier modifier
+ *
+[REDACTED_AUTHOR]
+ */
+@Composable
+internal fun AddCustomTokenWarning(description: TextReference, modifier: Modifier = Modifier) {
+ Card(
+ modifier = modifier,
+ shape = RoundedCornerShape(TangemTheme.dimens.radius4),
+ backgroundColor = TangemColorPalette.Tangerine,
+ contentColor = TangemColorPalette.White,
+ elevation = TangemTheme.dimens.elevation4,
+ ) {
+ Column(
+ modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
+ verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
+ ) {
+ Text(
+ text = stringResource(id = R.string.common_warning),
+ maxLines = 1,
+ style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
+ )
+ Text(
+ text = description.resolveReference(),
+ fontSize = 13.sp,
+ lineHeight = 18.sp,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContactAddressValidator.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContactAddressValidator.kt
new file mode 100644
index 0000000000..b522a922b1
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContactAddressValidator.kt
@@ -0,0 +1,42 @@
+package com.tangem.tap.features.customtoken.impl.presentation.validators
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchain.common.address.AddressService
+import com.tangem.common.card.EllipticCurve
+import com.tangem.domain.AddCustomTokenError
+
+/**
+ * Validator of contract address
+ *
+[REDACTED_AUTHOR]
+ */
+object ContactAddressValidator {
+
+ /** Validate a [address] using [blockchain] */
+ fun validate(address: String, blockchain: Blockchain): ContractAddressValidatorResult {
+ return when {
+ address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FieldIsEmpty)
+ validateAddress(blockchain, address) -> ContractAddressValidatorResult.Success
+ else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.InvalidContractAddress)
+ }
+ }
+
+ private fun validateAddress(blockchain: Blockchain, address: String): Boolean {
+ return when (blockchain) {
+ Blockchain.Unknown, Blockchain.Binance, Blockchain.BinanceTestnet -> {
+ SuccessAddressValidator.validate(address)
+ }
+ else -> {
+ blockchain.validateAddress(address)
+ }
+ }
+ }
+
+ private object SuccessAddressValidator : AddressService() {
+ override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String {
+ throw UnsupportedOperationException()
+ }
+
+ override fun validate(address: String): Boolean = true
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidatorResult.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidatorResult.kt
new file mode 100644
index 0000000000..417b3821e0
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/validators/ContractAddressValidatorResult.kt
@@ -0,0 +1,21 @@
+package com.tangem.tap.features.customtoken.impl.presentation.validators
+
+import com.tangem.domain.AddCustomTokenError
+
+/**
+ * Result of validation contract address
+ *
+[REDACTED_AUTHOR]
+ */
+sealed interface ContractAddressValidatorResult {
+
+ /** Success */
+ object Success : ContractAddressValidatorResult
+
+ /**
+ * Error
+ *
+ * @property type type of error
+ */
+ data class Error(val type: AddCustomTokenError) : ContractAddressValidatorResult
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt
new file mode 100644
index 0000000000..22b13b1216
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt
@@ -0,0 +1,612 @@
+package com.tangem.tap.features.customtoken.impl.presentation.viewmodels
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.blockchain.common.DerivationStyle
+import com.tangem.blockchain.common.Token
+import com.tangem.core.analytics.api.AnalyticsEventHandler
+import com.tangem.crypto.hdWallet.DerivationPath
+import com.tangem.domain.AddCustomTokenError
+import com.tangem.domain.common.TapWorkarounds.derivationStyle
+import com.tangem.domain.common.TapWorkarounds.isTestCard
+import com.tangem.domain.common.extensions.fromNetworkId
+import com.tangem.domain.common.extensions.isSupportedInApp
+import com.tangem.domain.common.extensions.supportedBlockchains
+import com.tangem.tap.common.analytics.events.ManageTokens
+import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TestTokenItem
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet.TokensCategoryBlock
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenInputField
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenSelectorField
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
+import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
+import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter
+import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
+import com.tangem.tap.features.customtoken.impl.presentation.validators.ContactAddressValidator
+import com.tangem.tap.features.customtoken.impl.presentation.validators.ContractAddressValidatorResult
+import com.tangem.tap.features.details.ui.cardsettings.TextReference
+import com.tangem.tap.features.wallet.models.Currency
+import com.tangem.tap.proxy.AppStateHolder
+import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
+import com.tangem.utils.coroutines.runCatching
+import com.tangem.wallet.BuildConfig
+import com.tangem.wallet.R
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.persistentListOf
+import kotlinx.coroutines.launch
+import timber.log.Timber
+import javax.inject.Inject
+
+/**
+ * ViewModel for add custom token screen
+ *
+ * @param featureRouter feature router
+ * @property featureInteractor feature interactor
+ * @property dispatchers coroutine dispatchers provider
+ * @property reduxStateHolder redux state holder
+ * @property analyticsEventHandler analytics event handler
+ *
+[REDACTED_AUTHOR]
+ */
+@HiltViewModel
+internal class AddCustomTokenViewModel @Inject constructor(
+ featureRouter: CustomTokenRouter,
+ private val featureInteractor: CustomTokenInteractor,
+ private val dispatchers: AppCoroutineDispatcherProvider,
+ private val reduxStateHolder: AppStateHolder,
+ private val analyticsEventHandler: AnalyticsEventHandler,
+) : ViewModel(), DefaultLifecycleObserver {
+
+ private val actionsHandler = ActionsHandler(featureRouter)
+ private val testActionsHandler = TestActionsHandler()
+
+ /** Screen state */
+ var uiState by mutableStateOf(getInitialUiState())
+ private set
+
+ private var foundTokenId: String? = null
+
+ override fun onCreate(owner: LifecycleOwner) {
+ analyticsEventHandler.send(ManageTokens.CustomToken.ScreenOpened)
+ }
+
+ private fun getInitialUiState(): AddCustomTokenStateHolder {
+ return if (BuildConfig.TEST_ACTION_ENABLED) {
+ AddCustomTokenStateHolder.TestContent(
+ onBackButtonClick = actionsHandler::onBackButtonClick,
+ toolbar = createToolbar(),
+ form = createForm(),
+ warnings = listOf(),
+ floatingButton = createFloatingButton(),
+ testBlock = AddCustomTokenTestBlock(
+ chooseTokenButtonText = "Choose token",
+ clearButtonText = "Clear address",
+ resetButtonText = "Reset",
+ onClearAddressButtonClick = testActionsHandler::onClearAddressButtonClick,
+ onResetButtonClick = testActionsHandler::onResetButtonClick,
+ ),
+ bottomSheet = AddCustomTokenChooseTokenBottomSheet(
+ categoriesBlocks = listOf(
+ TokensCategoryBlock(name = "Common", items = COMMON_TOKENS),
+ TokensCategoryBlock(name = "Solana", items = SOLANA_TOKENS),
+ ),
+ onTestTokenClick = actionsHandler::onContactAddressValueChange,
+ ),
+ )
+ } else {
+ AddCustomTokenStateHolder.Content(
+ onBackButtonClick = actionsHandler::onBackButtonClick,
+ toolbar = createToolbar(),
+ form = createForm(),
+ warnings = listOf(),
+ floatingButton = createFloatingButton(),
+ )
+ }
+ }
+
+ private fun createToolbar(): AddCustomTokensToolbar {
+ return AddCustomTokensToolbar(
+ title = TextReference.Res(R.string.add_custom_token_title),
+ onBackButtonClick = actionsHandler::onBackButtonClick,
+ )
+ }
+
+ private fun createForm(): AddCustomTokenForm {
+ return AddCustomTokenForm(
+ contractAddressInputField = createContractAddressInputField(),
+ networkSelectorField = createNetworkSelectorField(),
+ tokenNameInputField = createTokenNameInputField(),
+ tokenSymbolInputField = createTokenSymbolInputField(),
+ decimalsInputField = createDecimalsInputField(),
+ derivationPathSelectorField = createDerivationPathsSelectorField(),
+ )
+ }
+
+ private fun createContractAddressInputField(): AddCustomTokenInputField.ContactAddress {
+ return AddCustomTokenInputField.ContactAddress(
+ value = "",
+ onValueChange = actionsHandler::onContactAddressValueChange,
+ isError = false,
+ isLoading = false,
+ )
+ }
+
+ private fun createNetworkSelectorField(): AddCustomTokenSelectorField.Network {
+ val selectorItems = getNetworkSelectorItems()
+ return AddCustomTokenSelectorField.Network(
+ selectedItem = requireNotNull(selectorItems.firstOrNull()),
+ items = selectorItems,
+ onMenuItemClick = {
+ actionsHandler.onNetworkSelectorItemClick(
+ selectedItem = requireNotNull(selectorItems.getOrNull(it)),
+ )
+ },
+ )
+ }
+
+ private fun getNetworkSelectorItems(): List {
+ val card = reduxStateHolder.scanResponse?.card
+ val evmBlockchains = Blockchain.values().filter { card?.isTestCard == it.isTestnet() && it.isEvm() }
+
+ val additionalBlockchains = listOf(
+ Blockchain.Binance,
+ Blockchain.BinanceTestnet,
+ Blockchain.Solana,
+ Blockchain.SolanaTestnet,
+ Blockchain.Tron,
+ Blockchain.TronTestnet,
+ )
+
+ return (evmBlockchains + additionalBlockchains)
+ .filter { card?.supportedBlockchains()?.contains(it) == true }
+ .map(::createNetworkSelectorItem)
+ .toMutableList()
+ .apply {
+ add(index = 0, element = createNetworkSelectorItem(blockchain = Blockchain.Unknown))
+ }
+ }
+
+ private fun createNetworkSelectorItem(blockchain: Blockchain): AddCustomTokenSelectorField.SelectorItem.Title {
+ return when (blockchain) {
+ Blockchain.Unknown -> {
+ AddCustomTokenSelectorField.SelectorItem.Title(
+ title = TextReference.Res(R.string.custom_token_network_input_not_selected),
+ blockchain = Blockchain.Unknown,
+ )
+ }
+
+ else -> {
+ AddCustomTokenSelectorField.SelectorItem.Title(
+ title = TextReference.Str(blockchain.fullName),
+ blockchain = blockchain,
+ )
+ }
+ }
+ }
+
+ private fun createTokenNameInputField(): AddCustomTokenInputField.TokenName {
+ return AddCustomTokenInputField.TokenName(
+ value = "",
+ onValueChange = actionsHandler::onTokenNameValueChange,
+ isEnabled = false,
+ isError = false,
+ )
+ }
+
+ private fun createTokenSymbolInputField(): AddCustomTokenInputField.TokenSymbol {
+ return AddCustomTokenInputField.TokenSymbol(
+ value = "",
+ onValueChange = actionsHandler::onTokenSymbolValueChange,
+ isEnabled = false,
+ isError = false,
+ )
+ }
+
+ private fun createDecimalsInputField(): AddCustomTokenInputField.Decimals {
+ return AddCustomTokenInputField.Decimals(
+ value = "",
+ onValueChange = actionsHandler::onDecimalsValueChange,
+ isEnabled = false,
+ isError = false,
+ )
+ }
+
+ private fun createDerivationPathsSelectorField(): AddCustomTokenSelectorField.DerivationPath? {
+ if (reduxStateHolder.scanResponse?.card?.settings?.isHDWalletAllowed == false) return null
+
+ val selectorItems = getDerivationPathsSelectorItems()
+ return AddCustomTokenSelectorField.DerivationPath(
+ isEnabled = true,
+ selectedItem = requireNotNull(selectorItems.firstOrNull()),
+ items = selectorItems,
+ onMenuItemClick = {
+ val field = requireNotNull(uiState.form.derivationPathSelectorField)
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ derivationPathSelectorField = field.copy(
+ selectedItem = requireNotNull(selectorItems.getOrNull(it)),
+ ),
+ ),
+ )
+ },
+ )
+ }
+
+ private fun getDerivationPathsSelectorItems(): List {
+ val evmBlockchains = Blockchain.values().filter {
+ reduxStateHolder.scanResponse?.card?.isTestCard == it.isTestnet() && it.isEvm() && it.isSupportedInApp()
+ }
+
+ return evmBlockchains
+ .sortedBy(Blockchain::fullName)
+ .map(::createDerivationPathSelectorItem)
+ .toMutableList()
+ .apply {
+ add(index = 0, element = createDerivationPathSelectorItem(Blockchain.Unknown))
+ }
+ }
+
+ private fun createDerivationPathSelectorItem(
+ blockchain: Blockchain,
+ ): AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle {
+ return when (blockchain) {
+ Blockchain.Unknown -> {
+ AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle(
+ title = TextReference.Res(R.string.custom_token_derivation_path_default),
+ subtitle = TextReference.Res(R.string.custom_token_derivation_path_default),
+ blockchain = Blockchain.Unknown,
+ )
+ }
+
+ else -> {
+ AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle(
+ title = blockchain.derivationPath(DerivationStyle.LEGACY)?.rawPath?.let(TextReference::Str)
+ ?: TextReference.Res(R.string.custom_token_derivation_path_default),
+ subtitle = TextReference.Str(blockchain.fullName),
+ blockchain = blockchain,
+ )
+ }
+ }
+ }
+
+ private fun createFloatingButton(): AddCustomTokenFloatingButton {
+ return AddCustomTokenFloatingButton(isEnabled = false, onClick = actionsHandler::onAddCustomTokenClick)
+ }
+
+ private inner class ActionsHandler(private val featureRouter: CustomTokenRouter) {
+
+ fun onBackButtonClick() {
+ featureRouter.popBackStack()
+ }
+
+ fun onAddCustomTokenClick() {
+ if (uiState.form.networkSelectorField.selectedItem.blockchain != Blockchain.Unknown) {
+ val selectedNetwork = uiState.form.networkSelectorField.selectedItem.blockchain
+
+ val currency = if (isAnyTokenFieldsFilled() || isAllTokenFieldsFilled()) {
+ Currency.Token(
+ token = Token(
+ name = uiState.form.tokenNameInputField.value,
+ symbol = uiState.form.tokenSymbolInputField.value,
+ contractAddress = uiState.form.contractAddressInputField.value,
+ decimals = uiState.form.decimalsInputField.value.toInt(),
+ id = foundTokenId,
+ ),
+ blockchain = selectedNetwork,
+ derivationPath = getDerivationPath(
+ mainNetwork = selectedNetwork,
+ derivationNetwork = uiState.form.derivationPathSelectorField?.selectedItem?.blockchain,
+ derivationStyle = reduxStateHolder.scanResponse?.card?.derivationStyle,
+ )?.rawPath,
+ )
+ } else {
+ Currency.Blockchain(
+ blockchain = selectedNetwork,
+ derivationPath = getDerivationPath(
+ mainNetwork = selectedNetwork,
+ derivationNetwork = uiState.form.derivationPathSelectorField?.selectedItem?.blockchain,
+ derivationStyle = reduxStateHolder.scanResponse?.card?.derivationStyle,
+ )?.rawPath,
+ )
+ }
+
+ sendOnAddTokenButtonClick(currency = currency, address = uiState.form.contractAddressInputField.value)
+
+ viewModelScope.launch(dispatchers.io) {
+ featureInteractor.saveToken(
+ currency = currency,
+ address = uiState.form.contractAddressInputField.value,
+ )
+ }
+ }
+ }
+
+ fun onContactAddressValueChange(enteredValue: String) {
+ with(uiState.form) {
+ val selectedNetwork = networkSelectorField.selectedItem.blockchain
+ val isValid = ContactAddressValidator.validate(
+ address = enteredValue,
+ blockchain = selectedNetwork,
+ )
+
+ when (isValid) {
+ is ContractAddressValidatorResult.Success -> {
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ contractAddressInputField = contractAddressInputField.copy(
+ isError = false,
+ isLoading = true,
+ ),
+ ),
+ )
+ updateForm(address = enteredValue, selectedNetwork = selectedNetwork)
+ }
+
+ is ContractAddressValidatorResult.Error -> {
+ handleContractAddressErrorValidation(type = isValid.type)
+ }
+ }
+
+ updateDerivationPathSelector()
+
+ // TODO("[REDACTED_TASK_KEY] Update warnings")
+ // TODO("[REDACTED_TASK_KEY] Update floating button")
+ }
+ }
+
+ fun onNetworkSelectorItemClick(selectedItem: AddCustomTokenSelectorField.SelectorItem.Title) {
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ networkSelectorField = uiState.form.networkSelectorField.copy(selectedItem = selectedItem),
+ ),
+ )
+ onContactAddressValueChange(uiState.form.contractAddressInputField.value)
+ }
+
+ fun onTokenNameValueChange(enteredValue: String) {
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ tokenNameInputField = uiState.form.tokenNameInputField.copy(value = enteredValue),
+ ),
+ )
+ // TODO("[REDACTED_TASK_KEY] Update floating button")
+ }
+
+ fun onTokenSymbolValueChange(enteredValue: String) {
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ tokenSymbolInputField = uiState.form.tokenSymbolInputField.copy(value = enteredValue),
+ ),
+ )
+ // TODO("[REDACTED_TASK_KEY] Update floating button")
+ }
+
+ fun onDecimalsValueChange(enteredValue: String) {
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ decimalsInputField = uiState.form.decimalsInputField.copy(value = enteredValue),
+ ),
+ )
+ // TODO("[REDACTED_TASK_KEY] Update floating button")
+ }
+
+ private fun getDerivationPath(
+ mainNetwork: Blockchain,
+ derivationNetwork: Blockchain?,
+ derivationStyle: DerivationStyle?,
+ ): DerivationPath? {
+ val network = if (derivationNetwork == Blockchain.Unknown) mainNetwork else derivationNetwork
+
+ return network?.derivationPath(
+ style = if (derivationNetwork == Blockchain.Unknown) derivationStyle else DerivationStyle.LEGACY,
+ )
+ }
+
+ private fun sendOnAddTokenButtonClick(currency: Currency, address: String) {
+ when (currency) {
+ is Currency.Blockchain -> {
+ analyticsEventHandler.send(
+ ManageTokens.CustomToken.TokenWasAdded.Blockchain(
+ derivationPath = currency.derivationPath,
+ blockchain = currency.blockchain,
+ ),
+ )
+ }
+
+ is Currency.Token -> {
+ analyticsEventHandler.send(
+ ManageTokens.CustomToken.TokenWasAdded.Token(
+ symbol = currency.currencySymbol,
+ derivationPath = currency.derivationPath,
+ blockchain = currency.blockchain,
+ contractAddress = address,
+ ),
+ )
+ }
+ }
+ }
+
+ private fun isAnyTokenFieldsFilled(): Boolean {
+ return with(uiState.form) {
+ contractAddressInputField.value.isNotEmpty() || tokenNameInputField.value.isNotEmpty() ||
+ tokenSymbolInputField.value.isNotEmpty() || decimalsInputField.value.isNotEmpty()
+ }
+ }
+
+ private fun isAllTokenFieldsFilled(): Boolean {
+ return with(uiState.form) {
+ contractAddressInputField.value.isNotEmpty() && tokenNameInputField.value.isNotEmpty() &&
+ tokenSymbolInputField.value.isNotEmpty() && decimalsInputField.value.isNotEmpty()
+ }
+ }
+
+ private fun updateForm(address: String, selectedNetwork: Blockchain) {
+ viewModelScope.launch(dispatchers.main) {
+ runCatching(dispatchers.io) {
+ featureInteractor.findToken(address = address, blockchain = selectedNetwork)
+ }
+ .onSuccess { token ->
+ with(uiState.form) {
+ uiState = uiState.copySealed(
+ form = copy(
+ contractAddressInputField = contractAddressInputField.copy(isLoading = false),
+ networkSelectorField = networkSelectorField.copy(
+ selectedItem = createNetworkSelectorItem(
+ blockchain = Blockchain.fromNetworkId(token.network.id)
+ ?: Blockchain.Unknown,
+ ),
+ ),
+ tokenNameInputField = tokenNameInputField.copy(
+ value = token.name,
+ isEnabled = false,
+ ),
+ tokenSymbolInputField = tokenSymbolInputField.copy(
+ value = token.symbol,
+ isEnabled = false,
+ ),
+ decimalsInputField = decimalsInputField.copy(
+ value = token.network.decimalCount,
+ isEnabled = false,
+ ),
+ ),
+ )
+ }
+ }
+ .onFailure {
+ foundTokenId = null
+ Timber.e(it)
+ }
+ }
+ }
+
+ private fun handleContractAddressErrorValidation(type: AddCustomTokenError) {
+ with(uiState.form) {
+ val isNetworkSelectorFilled = networkSelectorField.selectedItem.blockchain != Blockchain.Unknown
+ val isAnotherTokenFieldsFilled = isAnyTokenFieldsFilled()
+
+ when {
+ isNetworkSelectorFilled && type == AddCustomTokenError.InvalidContractAddress -> {
+ // TODO("[REDACTED_TASK_KEY] Add error")
+
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ tokenNameInputField = tokenNameInputField.copy(isEnabled = isAnotherTokenFieldsFilled),
+ tokenSymbolInputField = tokenSymbolInputField.copy(
+ isEnabled = isAnotherTokenFieldsFilled,
+ ),
+ decimalsInputField = decimalsInputField.copy(isEnabled = isAnotherTokenFieldsFilled),
+ ),
+ )
+ }
+
+ !isNetworkSelectorFilled || type == AddCustomTokenError.FieldIsEmpty -> {
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ contractAddressInputField = contractAddressInputField.copy(isError = false),
+ tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false),
+ tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false),
+ decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false),
+ ),
+ )
+ }
+
+ else -> Unit
+ }
+ }
+ }
+
+ private fun updateDerivationPathSelector() {
+ val selectedValue = uiState.form.derivationPathSelectorField?.selectedItem?.blockchain ?: return
+ val isSupported = selectedValue.isEvm() || selectedValue == Blockchain.Unknown
+
+ if (selectedValue != Blockchain.Unknown && !isSupported) {
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ derivationPathSelectorField = uiState.form.derivationPathSelectorField?.copy(
+ selectedItem = createDerivationPathSelectorItem(Blockchain.Unknown),
+ ),
+ ),
+ )
+ }
+
+ if (uiState.form.derivationPathSelectorField?.isEnabled != isSupported) {
+ uiState = uiState.copySealed(
+ form = uiState.form.copy(
+ derivationPathSelectorField = uiState.form.derivationPathSelectorField?.copy(
+ isEnabled = isSupported,
+ ),
+ ),
+ )
+ }
+ }
+ }
+
+ private inner class TestActionsHandler {
+
+ fun onClearAddressButtonClick() {
+ with(uiState.form) {
+ uiState = uiState.copySealed(
+ form = copy(
+ contractAddressInputField = contractAddressInputField.copy(value = ""),
+ tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false),
+ tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false),
+ decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false),
+ ),
+ )
+ }
+ }
+
+ fun onResetButtonClick() {
+ with(uiState.form) {
+ uiState = uiState.copySealed(
+ form = copy(
+ contractAddressInputField = contractAddressInputField.copy(value = ""),
+ networkSelectorField = networkSelectorField.copy(
+ selectedItem = requireNotNull(networkSelectorField.items.firstOrNull()),
+ ),
+ tokenNameInputField = tokenNameInputField.copy(value = "", isEnabled = false),
+ tokenSymbolInputField = tokenSymbolInputField.copy(value = "", isEnabled = false),
+ decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false),
+ derivationPathSelectorField = derivationPathSelectorField?.copy(
+ selectedItem = requireNotNull(derivationPathSelectorField.items.firstOrNull()),
+ ),
+ ),
+ )
+ }
+ }
+ }
+
+ private companion object {
+ val COMMON_TOKENS = persistentListOf(
+ TestTokenItem(name = "USDC on ETH", address = "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48"),
+ TestTokenItem(name = "BUSD on ETH", address = "0x4fabb145d64652a948d72533023f6e7a623c7c53"),
+ TestTokenItem(name = "ETH on AVALANCHE", address = "0xf20d962a6c8f70c731bd838a3a388d7d48fa6e15"),
+ TestTokenItem(name = "USDC on ETH (invalid - cut address)", address = "0xa0b86991c6218b36c1d1"),
+ TestTokenItem(name = "Custom EVM", address = "0x1111111111111111112111111111111111111113"),
+ TestTokenItem(
+ name = "Supported by several networks",
+ address = "0xa1faa113cbe53436df28ff0aee54275c13b40975",
+ ),
+ TestTokenItem(name = "Invalid", address = "!@#_ _-%%^&&*((){P P2iOWsdfFQLA"),
+ )
+
+ val SOLANA_TOKENS = persistentListOf(
+ TestTokenItem(name = "USDT (full)", address = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB"),
+ TestTokenItem(
+ name = "USDT (valid - 2/3 of address)",
+ address = "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8Ben",
+ ),
+ TestTokenItem(name = "USDT (invalid - 1/3 of address)", address = "Es9vMFrzaCERmJ"),
+ TestTokenItem(name = "ETH (full)", address = "2FPyTwcZLUg1MDrwsyoP4D6s1tM7hAkHYRjkNb5w6Pxk"),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/addCustomToken/AddCustomTokenFragment.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt
similarity index 93%
rename from app/src/main/java/com/tangem/tap/features/addCustomToken/AddCustomTokenFragment.kt
rename to app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt
index 39689b35e5..280a46e758 100644
--- a/app/src/main/java/com/tangem/tap/features/addCustomToken/AddCustomTokenFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/AddCustomTokenFragment.kt
@@ -1,4 +1,4 @@
-package com.tangem.tap.features.addCustomToken
+package com.tangem.tap.features.customtoken.legacy
import android.os.Bundle
import android.view.View
@@ -19,7 +19,7 @@ import com.tangem.tap.common.compose.ClosePopupTrigger
import com.tangem.tap.features.BaseStoreFragment
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
-import com.tangem.tap.features.addCustomToken.compose.AddCustomTokenScreen
+import com.tangem.tap.features.customtoken.legacy.compose.AddCustomTokenScreen
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@@ -32,7 +32,7 @@ class AddCustomTokenFragment : BaseStoreFragment(R.layout.view_compose_fragment)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- Analytics.send(ManageTokens.CustomToken.ScreenOpened())
+ Analytics.send(ManageTokens.CustomToken.ScreenOpened)
}
override fun subscribeToStore() {
diff --git a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/AddCustomTokenScreen.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt
similarity index 97%
rename from app/src/main/java/com/tangem/tap/features/addCustomToken/compose/AddCustomTokenScreen.kt
rename to app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt
index 9342b868d1..88ba8be37d 100644
--- a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/AddCustomTokenScreen.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/AddCustomTokenScreen.kt
@@ -1,4 +1,4 @@
-package com.tangem.tap.features.addCustomToken.compose
+package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -52,8 +52,8 @@ import com.tangem.tap.common.compose.AddCustomTokenWarning
import com.tangem.tap.common.compose.ClosePopupTrigger
import com.tangem.tap.common.compose.ComposeDialogManager
import com.tangem.tap.domain.moduleMessage.ModuleMessageConverter
-import com.tangem.tap.features.addCustomToken.compose.test.TestCase
-import com.tangem.tap.features.addCustomToken.compose.test.TestCasesList
+import com.tangem.tap.features.customtoken.legacy.compose.test.TestCase
+import com.tangem.tap.features.customtoken.legacy.compose.test.TestCasesList
import com.tangem.wallet.R
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/FormFieldViews.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt
similarity index 99%
rename from app/src/main/java/com/tangem/tap/features/addCustomToken/compose/FormFieldViews.kt
rename to app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt
index 069d9f6823..9b5c87e6b7 100644
--- a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/FormFieldViews.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/FormFieldViews.kt
@@ -1,4 +1,4 @@
-package com.tangem.tap.features.addCustomToken.compose
+package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
diff --git a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/HangingOverKeyboardView.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt
similarity index 94%
rename from app/src/main/java/com/tangem/tap/features/addCustomToken/compose/HangingOverKeyboardView.kt
rename to app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt
index 54f0d74243..6aa2994210 100644
--- a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/HangingOverKeyboardView.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/HangingOverKeyboardView.kt
@@ -1,4 +1,4 @@
-package com.tangem.tap.features.addCustomToken.compose
+package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
diff --git a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/SelectTokenNetworkDialog.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt
similarity index 92%
rename from app/src/main/java/com/tangem/tap/features/addCustomToken/compose/SelectTokenNetworkDialog.kt
rename to app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt
index b30ab3c9e9..fd824e960f 100644
--- a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/SelectTokenNetworkDialog.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/SelectTokenNetworkDialog.kt
@@ -1,4 +1,4 @@
-package com.tangem.tap.features.addCustomToken.compose
+package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
diff --git a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/test/ContractAddressTests.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt
similarity index 98%
rename from app/src/main/java/com/tangem/tap/features/addCustomToken/compose/test/ContractAddressTests.kt
rename to app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt
index 6ba18d8a22..3ba171f427 100644
--- a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/test/ContractAddressTests.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/ContractAddressTests.kt
@@ -1,4 +1,4 @@
-package com.tangem.tap.features.addCustomToken.compose.test
+package com.tangem.tap.features.customtoken.legacy.compose.test
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
diff --git a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/test/TestCasesList.kt b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt
similarity index 96%
rename from app/src/main/java/com/tangem/tap/features/addCustomToken/compose/test/TestCasesList.kt
rename to app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt
index 7f92a0a666..3bae26e193 100644
--- a/app/src/main/java/com/tangem/tap/features/addCustomToken/compose/test/TestCasesList.kt
+++ b/app/src/main/java/com/tangem/tap/features/customtoken/legacy/compose/test/TestCasesList.kt
@@ -1,4 +1,4 @@
-package com.tangem.tap.features.addCustomToken.compose.test
+package com.tangem.tap.features.customtoken.legacy.compose.test
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt
index 43e31f423c..cccc4167ae 100644
--- a/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt
+++ b/app/src/main/java/com/tangem/tap/features/demo/DemoHelper.kt
@@ -9,7 +9,7 @@ import com.tangem.blockchain.common.toBlockchainSdkError
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.CompletionResult
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.demo.DemoConfig
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
diff --git a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt
index 81fcc9fc13..4624db8d34 100644
--- a/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/demo/DemoOnboardingNoteMiddleware.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.features.demo
import com.tangem.common.extensions.guard
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.demo.DemoConfig
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
diff --git a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt
index 1e97beb6e0..4805a4263a 100644
--- a/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt
+++ b/app/src/main/java/com/tangem/tap/features/demo/Extentions.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.features.demo
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
/**
[REDACTED_AUTHOR]
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt
index 6fe638daca..5782c7b0e2 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsAction.kt
@@ -1,9 +1,9 @@
package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.CardTypesResolver
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.Action
@@ -29,6 +29,15 @@ sealed class DetailsAction : Action {
data class PrepareCardSettingsData(val card: CardDTO, val cardTypesResolver: CardTypesResolver) : DetailsAction()
object ResetCardSettingsData : DetailsAction()
+ sealed class AccessCodeRecovery : DetailsAction() {
+ object Open : AccessCodeRecovery()
+ data class SaveChanges(val enabled: Boolean) : AccessCodeRecovery() {
+ data class Success(val enabled: Boolean) : AccessCodeRecovery()
+ }
+
+ data class SelectOption(val enabled: Boolean) : AccessCodeRecovery()
+ }
+
sealed class ManageSecurity : DetailsAction() {
object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt
index e7eab8cc49..cfcc202a2b 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsMiddleware.kt
@@ -7,8 +7,9 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.common.flatMap
import com.tangem.core.analytics.Analytics
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.extensions.dispatchDialogShow
@@ -51,6 +52,7 @@ class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val managePrivacyMiddleware = ManagePrivacyMiddleware()
+ private val accessCodeRecoveryMiddleware = AccessCodeRecoveryMiddleware()
val detailsMiddleware: Middleware = { _, stateProvider ->
{ next ->
{ action ->
@@ -74,6 +76,7 @@ class DetailsMiddleware {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
+ is DetailsAction.AccessCodeRecovery -> accessCodeRecoveryMiddleware.handle(state, action)
DetailsAction.ScanCard -> {
scope.launch {
tangemSdkManager.scanProduct(
@@ -418,4 +421,34 @@ class DetailsMiddleware {
)
}
}
+
+ class AccessCodeRecoveryMiddleware {
+ fun handle(state: DetailsState, action: DetailsAction.AccessCodeRecovery) {
+ when (action) {
+ is DetailsAction.AccessCodeRecovery.Open -> {
+ Analytics.send(Settings.CardSettings.AccessCodeRecoveryButton())
+ store.dispatch(NavigationAction.NavigateTo(AppScreen.AccessCodeRecovery))
+ }
+ is DetailsAction.AccessCodeRecovery.SaveChanges -> {
+ scope.launch {
+ tangemSdkManager
+ .setAccessCodeRecoveryEnabled(state.cardSettingsState?.card?.cardId, action.enabled)
+ .doOnSuccess {
+ Analytics.send(
+ Settings.CardSettings.AccessCodeRecoveryChanged(
+ AnalyticsParam.AccessCodeRecoveryStatus.from(action.enabled),
+ ),
+ )
+ store.dispatchOnMain(NavigationAction.PopBackTo())
+ store.dispatchOnMain(
+ DetailsAction.AccessCodeRecovery.SaveChanges.Success(action.enabled),
+ )
+ }
+ }
+ }
+ is DetailsAction.AccessCodeRecovery.SelectOption -> Unit
+ is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> Unit
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt
index 380d7be88b..85e80df897 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsReducer.kt
@@ -1,7 +1,8 @@
package com.tangem.tap.features.details.redux
-import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.CardTypesResolver
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.extensions.signedHashesCount
import com.tangem.tap.preferencesStorage
@@ -40,6 +41,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
}
is DetailsAction.ChangeAppCurrency ->
detailsState.copy(appCurrency = action.fiatCurrency)
+ is DetailsAction.AccessCodeRecovery -> handleAccessCodeRecoveryAction(action, detailsState)
else -> detailsState
}
}
@@ -68,6 +70,15 @@ private fun handlePrepareCardSettingsScreen(
manageSecurityState = prepareSecurityOptions(card, cardTypesResolver),
card = card,
resetCardAllowed = isResetToFactoryAllowedByCard(card, cardTypesResolver),
+ accessCodeRecovery = if (cardTypesResolver.isWallet2()) {
+ val enabled = card.userSettings?.isUserCodeRecoveryAllowed ?: false
+ AccessCodeRecoveryState(
+ enabledOnCard = enabled,
+ enabledSelection = enabled,
+ )
+ } else {
+ null
+ },
)
return state.copy(cardSettingsState = cardSettingsState)
}
@@ -189,6 +200,34 @@ private fun handlePrivacyAction(action: DetailsAction.AppSettings, state: Detail
}
}
+private fun handleAccessCodeRecoveryAction(
+ action: DetailsAction.AccessCodeRecovery,
+ state: DetailsState,
+): DetailsState {
+ return when (action) {
+ DetailsAction.AccessCodeRecovery.Open -> {
+ val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
+ enabledSelection = state.cardSettingsState.accessCodeRecovery.enabledOnCard,
+ )
+ state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
+ }
+ is DetailsAction.AccessCodeRecovery.SaveChanges -> state
+ is DetailsAction.AccessCodeRecovery.SelectOption -> {
+ val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
+ enabledSelection = action.enabled,
+ )
+ state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
+ }
+ is DetailsAction.AccessCodeRecovery.SaveChanges.Success -> {
+ val accessCodeRecovery = state.cardSettingsState?.accessCodeRecovery?.copy(
+ enabledOnCard = action.enabled,
+ enabledSelection = action.enabled,
+ )
+ state.copy(cardSettingsState = state.cardSettingsState?.copy(accessCodeRecovery = accessCodeRecovery))
+ }
+ }
+}
+
private fun prepareAllowedSecurityOptions(
cardTypesResolver: CardTypesResolver,
currentSecurityOption: SecurityOption?,
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt
index 62179b433d..6fd17a4119 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/DetailsState.kt
@@ -1,8 +1,8 @@
package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.FiatCurrency
import org.rekotlin.StateType
@@ -26,12 +26,22 @@ data class CardInfo(
val hasBackup: Boolean,
)
+/**
+ * @property enabledOnCard whether access code recovery is enabled on card
+ * @property enabledSelection current selected option in app (not saved on card yet)
+ */
+data class AccessCodeRecoveryState(
+ val enabledOnCard: Boolean,
+ val enabledSelection: Boolean,
+)
+
data class CardSettingsState(
val cardInfo: CardInfo,
val card: CardDTO,
val manageSecurityState: ManageSecurityState?,
val resetCardAllowed: Boolean,
val resetConfirmed: Boolean = false,
+ val accessCodeRecovery: AccessCodeRecoveryState? = null,
)
data class ManageSecurityState(
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt
index a383235538..9afb96f10f 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectAction.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.features.details.redux.walletconnect
import com.tangem.blockchain.common.Blockchain
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.wallet.R
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt
index 014ca34512..65dfeb1eab 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectMiddleware.kt
@@ -4,10 +4,11 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.guard
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
diff --git a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt
index 61fa67dd44..11bb3e6a8b 100644
--- a/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/redux/walletconnect/WalletConnectState.kt
@@ -6,7 +6,7 @@ import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.WalletManager
import com.tangem.crypto.hdWallet.DerivationPath
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialogData
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionRequestDialogData
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt
index be54f4a52f..44b2a45efe 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreen.kt
@@ -120,6 +120,7 @@ fun CardSettings(state: CardSettingsScreenState) {
is CardInfo.SignedHashes -> 14.dp
is CardInfo.SecurityMode -> 16.dp
is CardInfo.ChangeAccessCode -> 16.dp
+ is CardInfo.AccessCodeRecovery -> 16.dp
is CardInfo.ResetToFactorySettings -> 28.dp
}
val paddingTop = when (it) {
@@ -128,6 +129,7 @@ fun CardSettings(state: CardSettingsScreenState) {
is CardInfo.SignedHashes -> 12.dp
is CardInfo.SecurityMode -> 14.dp
is CardInfo.ChangeAccessCode -> 16.dp
+ is CardInfo.AccessCodeRecovery -> 16.dp
is CardInfo.ResetToFactorySettings -> 16.dp
}
Column(
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt
index 5ae42a55a9..9bb98f9e01 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsScreenState.kt
@@ -4,6 +4,7 @@ import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.res.stringResource
+import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.securitymode.toTitleRes
import com.tangem.tap.features.details.ui.utils.toResetCardDescriptionText
@@ -12,6 +13,7 @@ import com.tangem.tap.features.details.redux.CardInfo as ReduxCardInfo
data class CardSettingsScreenState(
val cardDetails: List? = null,
+ val accessCodeRecoveryState: AccessCodeRecoveryState? = null,
val onScanCardClick: () -> Unit,
val onElementClick: (CardInfo) -> Unit,
)
@@ -48,6 +50,16 @@ sealed class CardInfo(
clickable = true,
)
+ class AccessCodeRecovery(val enabled: Boolean) : CardInfo(
+ titleRes = TextReference.Res(R.string.card_settings_access_code_recovery_title),
+ subtitle = if (enabled) {
+ TextReference.Res(R.string.common_enabled)
+ } else {
+ TextReference.Res(R.string.common_disabled)
+ },
+ clickable = true,
+ )
+
class ResetToFactorySettings(cardInfo: ReduxCardInfo) : CardInfo(
titleRes = TextReference.Res(R.string.card_settings_reset_card_to_factory),
subtitle = cardInfo.toResetCardDescriptionText(),
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt
index 16b9836764..805f373605 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/CardSettingsViewModel.kt
@@ -16,6 +16,7 @@ class CardSettingsViewModel(private val store: Store) {
return if (state?.manageSecurityState == null) {
CardSettingsScreenState(
cardDetails = null,
+ accessCodeRecoveryState = null,
onElementClick = {},
onScanCardClick = {
store.dispatch(DetailsAction.ScanCard)
@@ -44,12 +45,15 @@ class CardSettingsViewModel(private val store: Store) {
if (state.card.backupStatus?.isActive == true && state.card.isAccessCodeSet) {
cardDetails.add(CardInfo.ChangeAccessCode)
}
+ if (state.accessCodeRecovery != null) {
+ cardDetails.add(CardInfo.AccessCodeRecovery(state.accessCodeRecovery.enabledOnCard))
+ }
if (state.resetCardAllowed) {
cardDetails.add(CardInfo.ResetToFactorySettings(state.cardInfo))
}
-
CardSettingsScreenState(
cardDetails = cardDetails,
+ accessCodeRecoveryState = state.accessCodeRecovery,
onScanCardClick = { },
onElementClick = {
handleClickingItem(it)
@@ -72,6 +76,9 @@ class CardSettingsViewModel(private val store: Store) {
Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode())
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
}
+ is CardInfo.AccessCodeRecovery -> {
+ store.dispatch(DetailsAction.AccessCodeRecovery.Open)
+ }
else -> {}
}
}
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt
new file mode 100644
index 0000000000..370d333d31
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryFragment.kt
@@ -0,0 +1,67 @@
+package com.tangem.tap.features.details.ui.cardsettings.coderecovery
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.platform.ComposeView
+import androidx.fragment.app.Fragment
+import androidx.transition.TransitionInflater
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.common.redux.navigation.NavigationAction
+import com.tangem.tap.features.details.redux.DetailsState
+import com.tangem.tap.store
+import com.tangem.wallet.R
+import org.rekotlin.StoreSubscriber
+
+class AccessCodeRecoveryFragment : Fragment(), StoreSubscriber {
+
+ private val viewModel = AccessCodeRecoveryViewModel(store)
+
+ private var screenState: MutableState =
+ mutableStateOf(viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery))
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val inflater = TransitionInflater.from(requireContext())
+ enterTransition = inflater.inflateTransition(R.transition.fade)
+ exitTransition = inflater.inflateTransition(R.transition.fade)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
+ return ComposeView(requireContext()).apply {
+ setContent {
+ isTransitionGroup = true
+ TangemTheme {
+ AccessCodeRecoveryScreen(
+ state = screenState.value,
+ onBackClick = { store.dispatch(NavigationAction.PopBackTo()) },
+ )
+ }
+ }
+ }
+ }
+
+ override fun onStart() {
+ super.onStart()
+ store.subscribe(this) { state ->
+ state.skipRepeats { oldState, newState ->
+ oldState.detailsState == newState.detailsState
+ }.select { it.detailsState }
+ }
+ }
+
+ override fun onStop() {
+ super.onStop()
+ store.unsubscribe(this)
+ }
+
+ override fun newState(state: DetailsState) {
+ if (activity == null || view == null) return
+ screenState.value =
+ viewModel.updateState(store.state.detailsState.cardSettingsState?.accessCodeRecovery)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt
new file mode 100644
index 0000000000..12edf5a47e
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreen.kt
@@ -0,0 +1,64 @@
+package com.tangem.tap.features.details.ui.cardsettings.coderecovery
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.features.details.ui.common.DetailsMainButton
+import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement
+import com.tangem.tap.features.details.ui.common.ScreenTitle
+import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
+import com.tangem.wallet.R
+
+@Composable
+fun AccessCodeRecoveryScreen(state: AccessCodeRecoveryScreenState, onBackClick: () -> Unit) {
+ SettingsScreensScaffold(
+ content = { AccessCodeRecoveryOptions(state = state) },
+ onBackClick = onBackClick,
+ )
+}
+
+@Composable
+fun AccessCodeRecoveryOptions(state: AccessCodeRecoveryScreenState) {
+ Column(
+ modifier = Modifier
+ .fillMaxSize()
+ .verticalScroll(rememberScrollState())
+ .padding(bottom = TangemTheme.dimens.spacing28),
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ ScreenTitle(
+ titleRes = R.string.card_settings_access_code_recovery_title,
+ Modifier.padding(bottom = TangemTheme.dimens.spacing36),
+ )
+
+ DetailsRadioButtonElement(
+ title = stringResource(id = R.string.common_enabled),
+ subtitle = stringResource(id = R.string.card_settings_access_code_recovery_enabled_description),
+ selected = state.enabledSelection,
+ onClick = { state.onOptionClick(true) },
+ )
+ DetailsRadioButtonElement(
+ title = stringResource(id = R.string.common_disabled),
+ subtitle = stringResource(id = R.string.card_settings_access_code_recovery_disabled_description),
+ selected = !state.enabledSelection,
+ onClick = { state.onOptionClick(false) },
+ )
+
+ Spacer(modifier = Modifier.weight(1f))
+
+ DetailsMainButton(
+ title = stringResource(id = R.string.common_save_changes),
+ enabled = state.isSaveChangesEnabled,
+ onClick = { state.onSaveChangesClick(state.enabledSelection) },
+ modifier = Modifier.padding(horizontal = TangemTheme.dimens.spacing20),
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt
new file mode 100644
index 0000000000..f1a825067a
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryScreenState.kt
@@ -0,0 +1,16 @@
+package com.tangem.tap.features.details.ui.cardsettings.coderecovery
+
+/**
+ * @property enabledOnCard Indicates whether access code recovery is enabled on the card
+ * @property enabledSelection Represents the currently selected option in the app (not yet saved on the card)
+ * @property isSaveChangesEnabled Determines if the user is allowed to save their selection to the card
+ * @property onSaveChangesClick Callback function called when the user wants to apply the selected option
+ * @property onOptionClick Callback function called when the user selects an option
+ * */
+data class AccessCodeRecoveryScreenState(
+ val enabledOnCard: Boolean,
+ val enabledSelection: Boolean,
+ val isSaveChangesEnabled: Boolean,
+ val onSaveChangesClick: (Boolean) -> Unit,
+ val onOptionClick: (Boolean) -> Unit,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt
new file mode 100644
index 0000000000..a2948075f8
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/cardsettings/coderecovery/AccessCodeRecoveryViewModel.kt
@@ -0,0 +1,30 @@
+package com.tangem.tap.features.details.ui.cardsettings.coderecovery
+
+import com.tangem.tap.common.redux.AppState
+import com.tangem.tap.features.details.redux.AccessCodeRecoveryState
+import com.tangem.tap.features.details.redux.DetailsAction
+import org.rekotlin.Store
+
+class AccessCodeRecoveryViewModel(val store: Store) {
+
+ fun updateState(state: AccessCodeRecoveryState?): AccessCodeRecoveryScreenState {
+ // We shouldn't get to this screen here when this state is null
+ return if (state == null) {
+ AccessCodeRecoveryScreenState(
+ enabledOnCard = false,
+ enabledSelection = false,
+ isSaveChangesEnabled = false,
+ onSaveChangesClick = {},
+ onOptionClick = {},
+ )
+ } else {
+ AccessCodeRecoveryScreenState(
+ enabledOnCard = state.enabledOnCard,
+ enabledSelection = state.enabledSelection,
+ isSaveChangesEnabled = state.enabledOnCard != state.enabledSelection,
+ onSaveChangesClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SaveChanges(it)) },
+ onOptionClick = { store.dispatch(DetailsAction.AccessCodeRecovery.SelectOption(it)) },
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt
index 616fbaafc9..c65fe7128b 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/common/DetailsComposeElements.kt
@@ -3,18 +3,18 @@ package com.tangem.tap.features.details.ui.common
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Button
-import androidx.compose.material.ButtonDefaults
+import androidx.compose.foundation.selection.selectable
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
+import androidx.compose.material.RadioButton
+import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
@@ -25,7 +25,9 @@ import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import com.tangem.core.ui.components.PrimaryButtonIconRight
import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.common.compose.TangemTypography
import com.tangem.wallet.R
@Composable
@@ -109,26 +111,49 @@ fun EmptyTopBarWithNavigation(
@Composable
fun DetailsMainButton(title: String, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true) {
- Button(
+ PrimaryButtonIconRight(
+ text = title,
+ enabled = enabled,
onClick = onClick,
modifier = modifier
+ .fillMaxWidth(),
+ icon = painterResource(id = R.drawable.ic_tangem_24),
+ )
+}
+
+@Composable
+fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean, onClick: () -> Unit) {
+ Row(
+ modifier = Modifier
.fillMaxWidth()
- .heightIn(48.dp),
- shape = RoundedCornerShape(12.dp),
- enabled = enabled,
- colors = ButtonDefaults.buttonColors(
- backgroundColor = colorResource(R.color.button_primary),
- contentColor = colorResource(R.color.text_primary_2),
- disabledBackgroundColor = colorResource(R.color.button_disabled),
- disabledContentColor = colorResource(R.color.text_disabled),
- ),
+ .selectable(
+ selected = selected,
+ onClick = { onClick() },
+ )
+ .padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp),
) {
- Text(text = title)
- Spacer(
- modifier = Modifier
- .padding(start = 20.dp, end = 20.dp)
- .size(8.dp),
+ RadioButton(
+ selected = selected,
+ onClick = null,
+ modifier = Modifier.padding(end = 20.dp),
+ colors = RadioButtonDefaults.colors(
+ unselectedColor = colorResource(id = R.color.icon_secondary),
+ selectedColor = colorResource(id = R.color.icon_accent),
+ ),
)
- Icon(painter = painterResource(id = R.drawable.ic_tangem_24), contentDescription = "")
+
+ Column {
+ Text(
+ text = title,
+ style = TangemTypography.subtitle1,
+ color = colorResource(id = R.color.text_primary_1),
+ )
+ Spacer(modifier = Modifier.size(4.dp))
+ Text(
+ text = subtitle,
+ style = TangemTypography.body2,
+ color = colorResource(id = R.color.text_secondary),
+ )
+ }
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt
index 7173d2ceb5..10edfda464 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/details/DetailsViewModel.kt
@@ -3,6 +3,7 @@ package com.tangem.tap.features.details.ui.details
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import com.tangem.core.analytics.Analytics
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
diff --git a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt
index 383fa7b498..ba9c01e84d 100644
--- a/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt
+++ b/app/src/main/java/com/tangem/tap/features/details/ui/securitymode/SecurityModeScreen.kt
@@ -2,27 +2,19 @@ package com.tangem.tap.features.details.ui.securitymode
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.RadioButton
-import androidx.compose.material.RadioButtonDefaults
-import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.details.ui.common.DetailsMainButton
+import com.tangem.tap.features.details.ui.common.DetailsRadioButtonElement
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@@ -74,39 +66,12 @@ fun SecurityOption(option: SecurityOption, state: SecurityModeScreenState) {
SecurityOption.AccessCode -> R.string.details_manage_security_access_code_description
}
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .selectable(
- selected = selected,
- onClick = { state.onNewModeSelected(option) },
- )
- .padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp),
- ) {
- RadioButton(
- selected = selected,
- onClick = null,
- modifier = Modifier.padding(end = 20.dp),
- colors = RadioButtonDefaults.colors(
- unselectedColor = colorResource(id = R.color.icon_secondary),
- selectedColor = colorResource(id = R.color.icon_accent),
- ),
- )
-
- Column {
- Text(
- text = stringResource(id = title),
- style = TangemTypography.subtitle1,
- color = colorResource(id = R.color.text_primary_1),
- )
- Spacer(modifier = Modifier.size(4.dp))
- Text(
- text = stringResource(id = subtitle),
- style = TangemTypography.body2,
- color = colorResource(id = R.color.text_secondary),
- )
- }
- }
+ DetailsRadioButtonElement(
+ title = stringResource(id = title),
+ subtitle = stringResource(id = subtitle),
+ selected = selected,
+ onClick = { state.onNewModeSelected(option) },
+ )
}
@Preview
diff --git a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt
index fb06687b57..30b3f43d1c 100644
--- a/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt
+++ b/app/src/main/java/com/tangem/tap/features/disclaimer/DisclaimerType.kt
@@ -1,6 +1,6 @@
package com.tangem.tap.features.disclaimer
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.tap.persistence.DisclaimerPrefStorage
diff --git a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt
index 3230f8b878..f534813ecb 100644
--- a/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/home/redux/HomeMiddleware.kt
@@ -5,7 +5,7 @@ import com.tangem.common.doOnResult
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.AnalyticsEvent
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.IntroductionProcess
import com.tangem.tap.common.analytics.events.Shop
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt
index fdf3a8b90b..cd92b90ea9 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingHelper.kt
@@ -4,8 +4,10 @@ import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
-import com.tangem.domain.common.ProductType
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.common.util.twinsIsTwinned
+import com.tangem.domain.models.scan.ProductType
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.extensions.removeContext
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 47caa1b85c..e35aa85d7a 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
@@ -5,7 +5,7 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.isZero
import com.tangem.common.services.Result
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.attestation.CardVerifyAndGetInfo
import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.common.extensions.isPositive
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt
index 53c844cd40..ab267f9dda 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/OnboardingSaltPayHelper.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.features.onboarding
import com.tangem.common.services.Result
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.extensions.successOr
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManager
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt
index 73312bd8a9..ba7983eaa5 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/otherCards/redux/OnboardingOtherCardsMiddleware.kt
@@ -3,6 +3,7 @@ package com.tangem.tap.features.onboarding.products.otherCards.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.core.analytics.Analytics
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.events.Onboarding
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt
index c552a354a4..4aa4e8a4d4 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsAction.kt
@@ -4,7 +4,7 @@ import com.tangem.Message
import com.tangem.blockchain.common.WalletManager
import com.tangem.common.extensions.VoidCallback
import com.tangem.datasource.asset.AssetReader
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt
index e7d4d1edf5..c2af40ad5d 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsMiddleware.kt
@@ -4,8 +4,9 @@ import com.tangem.blockchain.extensions.Result
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.domain.common.util.twinsIsTwinned
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
@@ -16,6 +17,7 @@ import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.getAddressData
import com.tangem.tap.common.extensions.getTopUpUrl
+import com.tangem.tap.common.extensions.onUserWalletSelected
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
@@ -135,6 +137,12 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
userWalletsListManager.delete(
listOfNotNull(UserWalletIdBuilder.scanResponse(getScanResponse()).build()),
)
+ userWalletsListManager.selectedUserWalletSync?.let { selectedWallet ->
+ store.onUserWalletSelected(
+ userWallet = selectedWallet,
+ sendAnalyticsEvent = false,
+ )
+ }
}
}
TwinCardsStep.TopUpWallet -> {
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt
index 9902cd2ce5..9ba9492c54 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsReducer.kt
@@ -1,5 +1,6 @@
package com.tangem.tap.features.onboarding.products.twins.redux
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt
index 88f9e5d82d..a5608d4371 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/redux/TwinCardsState.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.twins.TwinCardsManager
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt
index 4c8d89c813..1ef2872944 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/twins/ui/TwinsCardsFragment.kt
@@ -16,7 +16,7 @@ import com.tangem.common.extensions.VoidCallback
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.datasource.asset.AssetReader
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.TwinCardNumber
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.analytics.events.Onboarding
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt
index 4dad678741..a6eafe02d9 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletMiddleware.kt
@@ -5,10 +5,11 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.ifNotNull
import com.tangem.core.analytics.Analytics
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isSaltPay
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.extensions.withMainContext
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.backup.BackupService
import com.tangem.tap.backupService
import com.tangem.tap.common.analytics.events.Onboarding
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt
index 6e457704dd..0c50b1028f 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/redux/OnboardingWalletReducer.kt
@@ -1,5 +1,6 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.backupService
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt
index 409ca000e6..2f7f824f6c 100644
--- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt
+++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/saltPay/SaltPayActivationManager.kt
@@ -31,7 +31,7 @@ import com.tangem.datasource.api.paymentology.models.response.RegistrationRespon
import com.tangem.datasource.api.paymentology.models.response.tryExtractError
import com.tangem.datasource.config.models.KYCProvider
import com.tangem.datasource.config.models.SaltPayConfig
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.SaltPayWorkaround
import com.tangem.domain.common.extensions.successOr
import com.tangem.operations.attestation.AttestWalletKeyResponse
diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt
index 30ebdd258f..00a5c7413c 100644
--- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletAction.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.features.saveWallet.redux
import com.tangem.common.core.TangemError
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import org.rekotlin.Action
internal sealed interface SaveWalletAction : Action {
diff --git a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt
index 38af8d0b6f..b57528ceaa 100644
--- a/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt
+++ b/app/src/main/java/com/tangem/tap/features/saveWallet/redux/SaveWalletState.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.features.saveWallet.redux
import com.tangem.common.core.TangemError
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import org.rekotlin.StateType
data class SaveWalletState(
diff --git a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt
index 551d1b67b9..f24fb15f88 100644
--- a/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/send/redux/middlewares/SendMiddleware.kt
@@ -16,7 +16,7 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt
index 6e2bf1f5e9..9e9798eb3a 100644
--- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt
+++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt
@@ -320,7 +320,11 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
- llTotalContainer.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}")
+ llTotalContainer.tvTotalValue.post {
+ llTotalContainer.tvTotalValue.update(
+ "${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
+ )
+ }
val willSent = getString(
R.string.send_total_subtitle_format,
@@ -335,7 +339,9 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountCrypto} ${receipt.symbols.crypto}")
tvReceiptFeeValue.update("${receipt.feeCrypto} ${receipt.symbols.crypto}")
- llTotalContainer.tvTotalValue.update("${receipt.totalCrypto} ${receipt.symbols.crypto}")
+ llTotalContainer.tvTotalValue.post {
+ llTotalContainer.tvTotalValue.update("${receipt.totalCrypto} ${receipt.symbols.crypto}")
+ }
if (receipt.willSentFiat == UNKNOWN_AMOUNT_SIGN) {
llTotalContainer.tvWillBeSentValue.hide()
@@ -357,7 +363,11 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
totalTokenLayout.show(false)
tvReceiptAmountValue.update("${receipt.amountFiat} ${receipt.symbols.fiat}")
tvReceiptFeeValue.update("${receipt.feeFiat} ${receipt.symbols.fiat}")
- llTotalContainer.tvTotalValue.update("${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}")
+ llTotalContainer.tvTotalValue.post {
+ llTotalContainer.tvTotalValue.update(
+ "${roughOrEmpty(receipt.totalFiat)} ${receipt.symbols.fiat}",
+ )
+ }
val willSent = getString(
R.string.send_total_subtitle_asset_format,
diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt
index 290dfa225f..c7743263f4 100644
--- a/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt
+++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/domain/DefaultTokensListInteractor.kt
@@ -9,8 +9,9 @@ import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap
import com.tangem.crypto.hdWallet.DerivationPath
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
+import com.tangem.domain.common.util.supportsHdWallet
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
diff --git a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt
index 3e7e01c845..8cbfe2b322 100644
--- a/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt
+++ b/app/src/main/java/com/tangem/tap/features/tokens/impl/presentation/ui/TokensListScreen.kt
@@ -19,9 +19,15 @@ import androidx.compose.material.FabPosition
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
@@ -29,9 +35,10 @@ import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.ExperimentalUnitApi
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
+import androidx.compose.ui.unit.dp
import androidx.paging.PagingData
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
@@ -44,27 +51,46 @@ import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHo
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState
import com.tangem.wallet.R
import kotlinx.coroutines.flow.emptyFlow
-import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
/**
+ * Tokens list screen
+ *
+ * @param stateHolder state holder
+ *
[REDACTED_AUTHOR]
*/
@Composable
internal fun TokensListScreen(stateHolder: TokensListStateHolder) {
BackHandler(onBack = stateHolder.toolbarState.onBackButtonClick)
+ var floatingButtonHeight by remember { mutableStateOf(value = 0.dp) }
+
Scaffold(
topBar = { TokensListToolbar(state = stateHolder.toolbarState) },
floatingActionButton = {
if (stateHolder is TokensListStateHolder.ManageContent) {
- SaveChangesButton(onClick = stateHolder.onSaveButtonClick)
+ val density = LocalDensity.current
+ val verticalPadding = TangemTheme.dimens.spacing32
+
+ SaveChangesButton(
+ onClick = stateHolder.onSaveButtonClick,
+ modifier = Modifier.onSizeChanged {
+ with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding }
+ },
+ )
}
},
floatingActionButtonPosition = FabPosition.Center,
) { scaffoldPadding ->
val tokens = stateHolder.tokens.collectAsLazyPagingItems()
- TokensListContent(stateHolder.isDifferentAddressesBlockVisible, tokens, scaffoldPadding)
+ TokensListContent(
+ isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible,
+ tokens = tokens,
+ scaffoldPadding = scaffoldPadding,
+ bottomMarginDp = floatingButtonHeight,
+ )
stateHolder.onTokensLoadStateChanged(tokens.loadState.refresh)
@@ -96,6 +122,7 @@ private fun TokensListContent(
isDifferentAddressesBlockVisible: Boolean,
tokens: LazyPagingItems,
scaffoldPadding: PaddingValues,
+ bottomMarginDp: Dp,
) {
val state = rememberLazyListState()
@@ -108,6 +135,7 @@ private fun TokensListContent(
.fillMaxSize()
.padding(scaffoldPadding),
state = state,
+ contentPadding = PaddingValues(bottom = bottomMarginDp),
) {
if (isDifferentAddressesBlockVisible) {
item { DifferentAddressesWarning() }
@@ -119,7 +147,6 @@ private fun TokensListContent(
}
}
-@OptIn(ExperimentalUnitApi::class)
@Composable
private fun DifferentAddressesWarning() {
Box(
@@ -159,9 +186,9 @@ private fun DifferentAddressesWarning() {
}
@Composable
-private fun SaveChangesButton(onClick: () -> Unit) {
+private fun SaveChangesButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
PrimaryButton(
- modifier = Modifier
+ modifier = modifier
.imePadding()
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
@@ -205,16 +232,11 @@ private fun Preview_TokensListScreen_Manage() {
),
isLoading = false,
isDifferentAddressesBlockVisible = true,
- tokens = flow {
- emit(
- PagingData.from(
- listOf(
- TokenListPreviewData.createManageToken(),
- TokenListPreviewData.createManageToken(),
- ),
- ),
- )
- },
+ tokens = flowOf(
+ PagingData.from(
+ listOf(TokenListPreviewData.createManageToken()),
+ ),
+ ),
onSaveButtonClick = {},
onTokensLoadStateChanged = {},
),
@@ -235,16 +257,11 @@ private fun Preview_TokensListScreen_Read() {
),
isLoading = false,
isDifferentAddressesBlockVisible = false,
- tokens = flow {
- emit(
- PagingData.from(
- listOf(
- TokenListPreviewData.createManageToken(),
- TokenListPreviewData.createManageToken(),
- ),
- ),
- )
- },
+ tokens = flowOf(
+ PagingData.from(
+ listOf(TokenListPreviewData.createManageToken()),
+ ),
+ ),
onTokensLoadStateChanged = {},
),
)
diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt
index 2831a00547..bea0320bbe 100644
--- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensAction.kt
@@ -2,7 +2,7 @@ package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.domain.tokens.Currency
import org.rekotlin.Action
diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt
index dfb76ced9e..be311deb0a 100644
--- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensMiddleware.kt
@@ -12,12 +12,14 @@ import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.DomainWrapped
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.supportedBlockchains
+import com.tangem.domain.common.util.hasDerivation
+import com.tangem.domain.common.util.supportsHdWallet
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.domainStore
import com.tangem.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
diff --git a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt
index 432b422128..4f77d60fac 100644
--- a/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt
+++ b/app/src/main/java/com/tangem/tap/features/tokens/legacy/redux/TokensState.kt
@@ -3,7 +3,7 @@ package com.tangem.tap.features.tokens.legacy.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.blockchain.common.Token
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.fromNetworkId
import com.tangem.tap.domain.model.WalletDataModel
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt
index e4a508d068..18bf0dbc24 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/WalletAction.kt
@@ -105,9 +105,8 @@ sealed class WalletAction : Action {
sealed class TradeCryptoAction : WalletAction() {
object Sell : TradeCryptoAction()
- data class Buy(
- val checkUserLocation: Boolean = true,
- ) : TradeCryptoAction()
+
+ data class Buy(val checkUserLocation: Boolean = true) : TradeCryptoAction()
data class FinishSelling(val transactionId: String) : TradeCryptoAction()
data class SendCrypto(
@@ -134,5 +133,5 @@ sealed class WalletAction : Action {
data class UpdateUserWalletArtwork(val walletId: UserWalletId) : WalletAction()
- data class SetArtworkUrl(val url: String) : WalletAction()
+ data class SetArtworkUrl(val userWalletId: UserWalletId, val url: String) : WalletAction()
}
\ 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 dafb4c0324..a7e540bf43 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
@@ -3,6 +3,7 @@ package com.tangem.tap.features.wallet.redux
import android.graphics.Bitmap
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.WalletManager
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.toggleWidget.WidgetState
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt
index c12bebc8c7..5205339fca 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WalletMiddleware.kt
@@ -224,7 +224,11 @@ class WalletMiddleware {
)
},
)
- .doOnSuccess { store.dispatch(WalletAction.SetArtworkUrl(it.artworkUrl)) }
+ .doOnSuccess {
+ store.dispatch(
+ WalletAction.SetArtworkUrl(userWalletId = action.walletId, url = it.artworkUrl),
+ )
+ }
}
}
}
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt
index 9562b735fa..3ade28d8db 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/middlewares/WarningsMiddleware.kt
@@ -4,9 +4,10 @@ import com.tangem.blockchain.common.BlockchainSdkError
import com.tangem.blockchain.common.SignatureCountValidator
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.card.FirmwareVersion
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.isTestCard
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt
index 632ac3e7df..66d82deb98 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/redux/reducers/WalletReducer.kt
@@ -1,8 +1,8 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.Wallet
-import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isTestCard
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.model.WalletDataModel
@@ -14,6 +14,7 @@ import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.proxy.AppStateHolder
+import com.tangem.tap.userWalletsListManager
import org.rekotlin.Action
object WalletReducer {
@@ -115,9 +116,13 @@ private fun internalReduce(action: Action, state: AppState, appStateHolder: AppS
newState = newState.copy(canSaveUserWallets = action.canSaveUserWallets)
}
is WalletAction.SetArtworkUrl -> {
- newState = newState.copy(
- cardImage = Artwork(artworkId = action.url, artwork = newState.cardImage?.artwork),
- )
+ val selectedUserWallet = userWalletsListManager.selectedUserWalletSync?.walletId
+
+ if (selectedUserWallet == action.userWalletId) {
+ newState = newState.copy(
+ cardImage = Artwork(artworkId = action.url, artwork = newState.cardImage?.artwork),
+ )
+ }
}
else -> Unit
}
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt
index 7da258be29..d4556d790b 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletDetailsFragment.kt
@@ -1,11 +1,7 @@
package com.tangem.tap.features.wallet.ui
import android.os.Bundle
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import android.view.View
-import android.view.ViewGroup
+import android.view.*
import android.widget.TextView
import androidx.activity.OnBackPressedCallback
import androidx.annotation.ColorRes
@@ -31,14 +27,7 @@ import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.TestActions
import com.tangem.tap.common.analytics.events.DetailsScreen
import com.tangem.tap.common.analytics.events.Token
-import com.tangem.tap.common.extensions.appendIfNotNull
-import com.tangem.tap.common.extensions.beginDelayedTransition
-import com.tangem.tap.common.extensions.fitChipsByGroupWidth
-import com.tangem.tap.common.extensions.getColor
-import com.tangem.tap.common.extensions.getString
-import com.tangem.tap.common.extensions.hide
-import com.tangem.tap.common.extensions.show
-import com.tangem.tap.common.extensions.toQrCode
+import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.recyclerView.SpaceItemDecoration
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.utils.SafeStoreSubscriber
@@ -56,15 +45,7 @@ import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.adapters.WalletDetailWarningMessagesAdapter
import com.tangem.tap.features.wallet.ui.images.load
import com.tangem.tap.features.wallet.ui.test.TestWallet
-import com.tangem.tap.features.wallet.ui.utils.assembleWarnings
-import com.tangem.tap.features.wallet.ui.utils.getAvailableActions
-import com.tangem.tap.features.wallet.ui.utils.getFormattedAmount
-import com.tangem.tap.features.wallet.ui.utils.getFormattedFiatAmount
-import com.tangem.tap.features.wallet.ui.utils.isAvailableToBuy
-import com.tangem.tap.features.wallet.ui.utils.isAvailableToSell
-import com.tangem.tap.features.wallet.ui.utils.isAvailableToSwap
-import com.tangem.tap.features.wallet.ui.utils.mainButton
-import com.tangem.tap.features.wallet.ui.utils.shouldShowMultipleAddress
+import com.tangem.tap.features.wallet.ui.utils.*
import com.tangem.tap.store
import com.tangem.tap.userWalletsListManagerSafe
import com.tangem.tap.walletCurrenciesManager
@@ -76,6 +57,9 @@ import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
+/**
+ * Wallet details fragment - use only for MultiWallet
+ */
@Suppress("LargeClass", "MagicNumber")
@AndroidEntryPoint
class WalletDetailsFragment :
@@ -308,7 +292,11 @@ class WalletDetailsFragment :
WalletAction.DialogAction.ChooseTradeActionDialog(
buyAllowed = selectedWallet.isAvailableToBuy(exchangeManager),
sellAllowed = selectedWallet.isAvailableToSell(exchangeManager),
- swapAllowed = selectedWallet.isAvailableToSwap(swapFeatureToggleManager, swapInteractor),
+ swapAllowed = selectedWallet.isAvailableToSwap(
+ swapFeatureToggleManager = swapFeatureToggleManager,
+ swapInteractor = swapInteractor,
+ isSingleWallet = false,
+ ),
),
)
}
@@ -317,6 +305,7 @@ class WalletDetailsFragment :
swapInteractor = swapInteractor,
exchangeManager = exchangeManager,
swapFeatureToggleManager = swapFeatureToggleManager,
+ isSingleWallet = false,
)
binding.rowButtons.updateButtonsVisibility(
actions = actions,
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt
index 3bf4cd8b80..1d0cc810ed 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletFragment.kt
@@ -7,8 +7,12 @@ import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.mutableStateOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.flowWithLifecycle
+import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.transition.TransitionInflater
@@ -19,6 +23,8 @@ import com.badoo.mvicore.modelWatcher
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.fragments.setStatusBarColor
import com.tangem.core.ui.utils.OneTouchClickListener
+import com.tangem.datasource.connection.NetworkConnectionManager
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.feature.swap.api.SwapFeatureToggleManager
import com.tangem.feature.swap.domain.SwapInteractor
import com.tangem.tap.MainActivity
@@ -47,6 +53,8 @@ import com.tangem.wallet.BuildConfig
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.launch
import javax.inject.Inject
@AndroidEntryPoint
@@ -58,6 +66,9 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber
state.select { it.walletState }
}
@@ -194,8 +209,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber) {
warningsAdapter.submitList(warnings)
binding.rvWarningMessages.show(warnings.isNotEmpty())
@@ -215,13 +234,17 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), SafeStoreSubscriber
+ if (isOnline && isNetworkConnectionError.value) {
+ refreshWalletData()
+ }
+ }
+ }
+ }
+
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.details_menu -> {
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt
index bba9814685..ce2e4ea085 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/WalletViewModel.kt
@@ -5,6 +5,7 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.tangem.core.analytics.api.AnalyticsEventHandler
+import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.analytics.events.MainScreen
@@ -80,14 +81,6 @@ internal class WalletViewModel @Inject constructor(
analyticsEventHandler.send(MainScreen.ScreenOpened())
}
- private fun launch() {
- val manager = store.state.globalState.userWalletsListManager
- if (manager != null) {
- bootstrapSelectedWalletStoresChanges(manager)
- }
- bootstrapShowSaveWalletIfNeeded()
- }
-
fun onBalanceLoaded(totalBalance: TotalFiatBalance?) {
if (totalBalance != null) {
walletAnalyticsEventsMapper.convert(totalBalance)?.let { balanceParam ->
@@ -100,6 +93,14 @@ internal class WalletViewModel @Inject constructor(
}
}
+ private fun launch() {
+ val manager = store.state.globalState.userWalletsListManager
+ if (manager != null) {
+ bootstrapSelectedWalletStoresChanges(manager)
+ }
+ bootstrapShowSaveWalletIfNeeded()
+ }
+
@OptIn(FlowPreview::class)
private fun bootstrapSelectedWalletStoresChanges(manager: UserWalletsListManager) {
observeWalletStoresUpdatesJob = manager.selectedUserWallet
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/utils/WalletDataOperations.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/utils/WalletDataOperations.kt
index 42be04542e..042426cb80 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/ui/utils/WalletDataOperations.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/utils/WalletDataOperations.kt
@@ -56,7 +56,11 @@ internal fun WalletDataModel.isAvailableToSell(exchangeManager: CurrencyExchange
internal fun WalletDataModel.isAvailableToSwap(
swapFeatureToggleManager: SwapFeatureToggleManager,
swapInteractor: SwapInteractor,
+ isSingleWallet: Boolean,
): Boolean {
+ if (isSingleWallet) {
+ return false
+ }
if (currency.blockchain.id == Blockchain.Optimism.id && !swapFeatureToggleManager.isOptimismSwapEnabled) {
return false
}
@@ -68,11 +72,12 @@ internal fun WalletDataModel.getAvailableActions(
swapInteractor: SwapInteractor,
exchangeManager: CurrencyExchangeManager,
swapFeatureToggleManager: SwapFeatureToggleManager,
+ isSingleWallet: Boolean,
): Set {
return setOfNotNull(
if (isAvailableToBuy(exchangeManager)) CurrencyAction.Buy else null,
if (isAvailableToSell(exchangeManager)) CurrencyAction.Sell else null,
- if (isAvailableToSwap(swapFeatureToggleManager, swapInteractor)) CurrencyAction.Swap else null,
+ if (isAvailableToSwap(swapFeatureToggleManager, swapInteractor, isSingleWallet)) CurrencyAction.Swap else null,
)
}
diff --git a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt
index 5edef28db5..5668693e18 100644
--- a/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt
+++ b/app/src/main/java/com/tangem/tap/features/wallet/ui/wallet/SingleWalletView.kt
@@ -5,12 +5,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Token
-import com.tangem.tap.common.extensions.beginDelayedTransition
-import com.tangem.tap.common.extensions.fitChipsByGroupWidth
-import com.tangem.tap.common.extensions.getQuantityString
-import com.tangem.tap.common.extensions.getString
-import com.tangem.tap.common.extensions.hide
-import com.tangem.tap.common.extensions.show
+import com.tangem.tap.common.extensions.*
import com.tangem.tap.domain.model.WalletDataModel
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.Currency
@@ -23,11 +18,7 @@ import com.tangem.tap.features.wallet.ui.BalanceWidget
import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
-import com.tangem.tap.features.wallet.ui.utils.getAvailableActions
-import com.tangem.tap.features.wallet.ui.utils.isAvailableToBuy
-import com.tangem.tap.features.wallet.ui.utils.isAvailableToSell
-import com.tangem.tap.features.wallet.ui.utils.mainButton
-import com.tangem.tap.features.wallet.ui.utils.shouldShowMultipleAddress
+import com.tangem.tap.features.wallet.ui.utils.*
import com.tangem.tap.features.wallet.ui.view.WalletDetailsButtonsRow
import com.tangem.tap.store
import com.tangem.wallet.R
@@ -179,6 +170,7 @@ class SingleWalletView : WalletView() {
swapInteractor = swapInteractor,
exchangeManager = exchangeManager,
swapFeatureToggleManager = swapFeatureToggleManager,
+ isSingleWallet = true,
)
binding?.rowButtons?.updateButtonsVisibility(
actions = actions,
diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt
index aadd56356b..675ca89cdf 100644
--- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorMiddleware.kt
@@ -7,7 +7,7 @@ import com.tangem.common.doOnSuccess
import com.tangem.common.flatMap
import com.tangem.common.map
import com.tangem.core.analytics.Analytics
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.common.util.UserWalletId
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt
index c28a1e4ab1..2d06b37937 100644
--- a/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt
+++ b/app/src/main/java/com/tangem/tap/features/walletSelector/redux/WalletSelectorReducer.kt
@@ -1,6 +1,7 @@
package com.tangem.tap.features.walletSelector.redux
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.common.util.cardTypesResolver
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.model.TotalFiatBalance
import com.tangem.tap.domain.model.UserWallet
diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt
index 9f82671d66..4f24d56193 100644
--- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt
+++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorBottomSheetFragment.kt
@@ -25,6 +25,7 @@ import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
+import com.tangem.tap.features.walletSelector.ui.components.BiometricsDisabledWarningContent
import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutWarningContent
import com.tangem.tap.features.walletSelector.ui.components.KeyInvalidatedWarningContent
import com.tangem.tap.features.walletSelector.ui.components.RemoveWalletDialogContent
@@ -96,6 +97,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment RenameWalletDialogContent(dialog)
is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog)
is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog)
+ is WarningModel.BiometricsDisabledWarning -> BiometricsDisabledWarningContent(dialog)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt
index 0bdae60f69..99024dad35 100644
--- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/WalletSelectorViewModel.kt
@@ -196,7 +196,11 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber WarningModel.KeyInvalidatedWarning(
+ is UserWalletsListError.EncryptionKeyInvalidated -> WarningModel.KeyInvalidatedWarning(
+ onDismiss = this::dismissWarningDialog,
+ )
+ is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
+ onConfirm = { /* [REDACTED_TODO_COMMENT] */ },
onDismiss = this::dismissWarningDialog,
)
else -> currentDialog
diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsDisabledWarningContent.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsDisabledWarningContent.kt
new file mode 100644
index 0000000000..429ab5471e
--- /dev/null
+++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/components/BiometricsDisabledWarningContent.kt
@@ -0,0 +1,55 @@
+package com.tangem.tap.features.walletSelector.ui.components
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.tangem.core.ui.components.BasicDialog
+import com.tangem.core.ui.components.DialogButton
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.tap.features.walletSelector.ui.model.WarningModel
+import com.tangem.wallet.R
+
+@Composable
+internal fun BiometricsDisabledWarningContent(warning: WarningModel.BiometricsDisabledWarning) {
+ BasicDialog(
+ title = stringResource(id = R.string.common_warning),
+ message = stringResource(id = R.string.biometric_unavailable_warning),
+ onDismissDialog = warning.onDismiss,
+ confirmButton = DialogButton(
+ title = stringResource(id = R.string.common_ok),
+ onClick = warning.onDismiss,
+ ),
+ )
+}
+
+// region Preview
+@Composable
+private fun BiometricsDisabledWarningContentSample(modifier: Modifier = Modifier) {
+ Column(modifier = modifier) {
+ BiometricsDisabledWarningContent(
+ warning = WarningModel.BiometricsDisabledWarning(
+ onConfirm = {},
+ onDismiss = {},
+ ),
+ )
+ }
+}
+
+@Preview(showBackground = true, widthDp = 360)
+@Composable
+private fun BiometricsDisabledWarningContentPreview_Light() {
+ TangemTheme {
+ BiometricsDisabledWarningContentSample()
+ }
+}
+
+@Preview(showBackground = true, widthDp = 360)
+@Composable
+private fun BiometricsDisabledWarningContentPreview_Dark() {
+ TangemTheme(isDark = true) {
+ BiometricsDisabledWarningContentSample()
+ }
+}
+// endregion Preview
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt
index 8024e00021..b458313c35 100644
--- a/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/walletSelector/ui/model/DialogModel.kt
@@ -22,4 +22,9 @@ internal sealed interface WarningModel : DialogModel {
data class KeyInvalidatedWarning(
val onDismiss: () -> Unit,
) : WarningModel
+
+ data class BiometricsDisabledWarning(
+ val onConfirm: () -> Unit,
+ val onDismiss: () -> Unit,
+ ) : WarningModel
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt
index e728d10a4a..6e7da12221 100644
--- a/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt
+++ b/app/src/main/java/com/tangem/tap/features/welcome/redux/WelcomeMiddleware.kt
@@ -4,7 +4,7 @@ import android.content.Intent
import com.tangem.common.core.TangemSdkError
import com.tangem.common.doOnFailure
import com.tangem.common.doOnSuccess
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Basic
import com.tangem.tap.common.extensions.dispatchOnMain
diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt
index 4bbcdf76ea..e19891caa2 100644
--- a/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/WelcomeViewModel.kt
@@ -68,7 +68,11 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber {
isPermanent = error.isPermanent,
onDismiss = this::dismissWarning,
)
- is UserWalletsListError.InvalidEncryptionKey -> WarningModel.KeyInvalidatedWarning(
+ is UserWalletsListError.EncryptionKeyInvalidated -> WarningModel.KeyInvalidatedWarning(
+ onDismiss = this::dismissWarning,
+ )
+ is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
+ onConfirm = { /* [REDACTED_TODO_COMMENT] */ },
onDismiss = this::dismissWarning,
)
else -> null
diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt
index 40ae13b0cb..d8a78e246f 100644
--- a/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt
+++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/components/WarningDialog.kt
@@ -43,6 +43,17 @@ internal fun WarningDialog(warning: WarningModel?) {
),
)
}
+ is WarningModel.BiometricsDisabledWarning -> {
+ BasicDialog(
+ title = stringResource(id = R.string.common_warning),
+ message = stringResource(id = R.string.biometric_unavailable_warning),
+ onDismissDialog = warning.onDismiss,
+ confirmButton = DialogButton(
+ title = stringResource(id = R.string.common_ok),
+ onClick = warning.onConfirm,
+ ),
+ )
+ }
}
}
@@ -103,7 +114,6 @@ private fun BiometricsLockoutDialog_Permanent_Preview_Dark() {
}
}
-// region Preview
@Composable
private fun KeyInvalidatedWarningSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
@@ -126,5 +136,27 @@ private fun KeyInvalidatedWarningPreview_Dark() {
KeyInvalidatedWarningSample()
}
}
-// endregion Preview
+
+@Composable
+private fun BiometricDisabledWarningSample(modifier: Modifier = Modifier) {
+ Column(modifier = modifier) {
+ WarningDialog(warning = WarningModel.BiometricsDisabledWarning(onConfirm = {}, onDismiss = {}))
+ }
+}
+
+@Preview(showBackground = true, widthDp = 360)
+@Composable
+private fun BiometricDisabledWarningPreview_Light() {
+ TangemTheme {
+ BiometricDisabledWarningSample()
+ }
+}
+
+@Preview(showBackground = true, widthDp = 360)
+@Composable
+private fun BiometricDisabledWarningPreview_Dark() {
+ TangemTheme(isDark = true) {
+ BiometricDisabledWarningSample()
+ }
+}
// endregion Preview
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt b/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt
index 4940a4642a..55789fe91a 100644
--- a/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt
+++ b/app/src/main/java/com/tangem/tap/features/welcome/ui/model/WarningModel.kt
@@ -9,4 +9,9 @@ internal sealed interface WarningModel {
data class KeyInvalidatedWarning(
val onDismiss: () -> Unit,
) : WarningModel
+
+ data class BiometricsDisabledWarning(
+ val onConfirm: () -> Unit,
+ val onDismiss: () -> Unit,
+ ) : WarningModel
}
\ No newline at end of file
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt
index d77245b45c..fec559a528 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/BuyExchangeService.kt
@@ -1,7 +1,7 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
-import com.tangem.domain.common.ProductType
+import com.tangem.domain.models.scan.ProductType
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
import com.tangem.tap.network.exchangeServices.utorg.UtorgExchangeService
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt
index 939121bda7..3e0d7ab0a5 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CardExchangeRules.kt
@@ -1,6 +1,6 @@
package com.tangem.tap.network.exchangeServices
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.Currency
diff --git a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt
index f7beab3570..a73937a8ee 100644
--- a/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt
+++ b/app/src/main/java/com/tangem/tap/network/exchangeServices/CurrencyExchangeManager.kt
@@ -7,7 +7,7 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.blockchain.extensions.Result
-import com.tangem.domain.common.CardDTO
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.common.extensions.safeUpdate
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
diff --git a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt
index ed21a083df..aab3cfd808 100644
--- a/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt
+++ b/app/src/main/java/com/tangem/tap/proxy/AppStateHolder.kt
@@ -1,8 +1,8 @@
package com.tangem.tap.proxy
import com.tangem.TangemSdk
-import com.tangem.domain.common.CardDTO
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TangemSdkManager
diff --git a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt
index f52ac33f8a..72645564d0 100644
--- a/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt
+++ b/app/src/main/java/com/tangem/tap/proxy/DerivationManagerImpl.kt
@@ -9,9 +9,10 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.extensions.toMapKey
import com.tangem.crypto.hdWallet.DerivationPath
-import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.fromNetworkId
+import com.tangem.domain.common.util.hasDerivation
+import com.tangem.domain.models.scan.ScanResponse
import com.tangem.lib.crypto.DerivationManager
import com.tangem.lib.crypto.models.Currency
import com.tangem.lib.crypto.models.Currency.NonNativeToken
diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt
index 57cdad3cf3..65a4cdb3e9 100644
--- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt
+++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphAction.kt
@@ -3,6 +3,7 @@ package com.tangem.tap.proxy.redux
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.features.tester.api.TesterRouter
+import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
import org.rekotlin.Action
@@ -12,6 +13,7 @@ sealed interface DaggerGraphAction : Action {
val assetReader: AssetReader,
val networkConnectionManager: NetworkConnectionManager,
val tokensListFeatureToggles: TokensListFeatureToggles,
+ val customTokenFeatureToggles: CustomTokenFeatureToggles,
) : DaggerGraphAction
data class SetActivityDependencies(val testerRouter: TesterRouter) : DaggerGraphAction
diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt
index 65cfee1cf5..3bb0e65284 100644
--- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt
+++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphReducer.kt
@@ -16,6 +16,7 @@ object DaggerGraphReducer {
assetReader = action.assetReader,
networkConnectionManager = action.networkConnectionManager,
tokensListFeatureToggles = action.tokensListFeatureToggles,
+ customTokenFeatureToggles = action.customTokenFeatureToggles,
)
is DaggerGraphAction.SetActivityDependencies -> state.daggerGraphState.copy(
testerRouter = action.testerRouter,
diff --git a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt
index 792009c602..dd0a501489 100644
--- a/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt
+++ b/app/src/main/java/com/tangem/tap/proxy/redux/DaggerGraphState.kt
@@ -3,6 +3,7 @@ package com.tangem.tap.proxy.redux
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.connection.NetworkConnectionManager
import com.tangem.features.tester.api.TesterRouter
+import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
import org.rekotlin.StateType
@@ -11,6 +12,7 @@ data class DaggerGraphState(
val testerRouter: TesterRouter? = null,
val networkConnectionManager: NetworkConnectionManager? = null,
val tokensListFeatureToggles: TokensListFeatureToggles? = null,
+ val customTokenFeatureToggles: CustomTokenFeatureToggles? = null,
) : StateType {
inline fun get(getDependency: DaggerGraphState.() -> T?): T {
diff --git a/app/src/main/res/drawable/ic_onboarding_chat_24.xml b/app/src/main/res/drawable/ic_onboarding_chat_24.xml
new file mode 100644
index 0000000000..a54ee4625a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_chat_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_onboarding_qr_code_scan_24.xml b/app/src/main/res/drawable/ic_onboarding_qr_code_scan_24.xml
new file mode 100644
index 0000000000..93e77daea8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_onboarding_qr_code_scan_24.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_ravencoin_no_color.xml b/app/src/main/res/drawable/ic_ravencoin_no_color.xml
new file mode 100644
index 0000000000..5bc49c57cf
--- /dev/null
+++ b/app/src/main/res/drawable/ic_ravencoin_no_color.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
diff --git a/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt b/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt
index 11da28c037..ef7941cc63 100644
--- a/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt
+++ b/common/src/main/java/com/tangem/common/module/ModuleErrorCode.kt
@@ -10,4 +10,5 @@ object ModuleErrorCode {
const val NETWORK = 20000
const val DOMAIN = 30000
const val SALT_PAY = 40000
+ const val ONBOARDING = 50000
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt
index 7e33fefae3..ce19ff204e 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AndroidAssetReader.kt
@@ -3,6 +3,7 @@ package com.tangem.datasource.asset
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.BufferedReader
+import java.io.InputStream
import javax.inject.Inject
/**
@@ -15,9 +16,13 @@ internal class AndroidAssetReader @Inject constructor(
) : AssetReader {
override fun readJson(fileName: String): String {
- return context.assets
- .open("$fileName.json")
+ return openFile("$fileName.json")
.bufferedReader()
.use(BufferedReader::readText)
}
+
+ override fun openFile(fileName: String): InputStream {
+ return context.assets
+ .open(fileName)
+ }
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt
index 6475ad3909..a3196c64aa 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/asset/AssetReader.kt
@@ -1,5 +1,7 @@
package com.tangem.datasource.asset
+import java.io.InputStream
+
/**
* Asset file reader
*
@@ -9,4 +11,7 @@ interface AssetReader {
/** Read content of json file [fileName] from asset */
fun readJson(fileName: String): String
+
+ /** Open a file [file] from asset as InputStream */
+ fun openFile(file: String): InputStream
}
\ No newline at end of file
diff --git a/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt b/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt
index 695dbce8bd..6581fbb384 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/connection/AndroidNetworkConnectionManager.kt
@@ -15,6 +15,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@@ -35,12 +36,13 @@ internal class AndroidNetworkConnectionManager @Inject constructor(
private val dispatchers: CoroutineDispatcherProvider,
) : NetworkConnectionManager {
- override val isOnline: Boolean get() = _isOnline.value
-
private val _isOnline = MutableStateFlow(value = false)
private val callbacks = NetworkConnectionManagerCallbacks()
private val receiver = NetworkConnectionBroadcastReceiver()
+ override val isOnline: Boolean get() = _isOnline.value
+ override val isOnlineFlow: StateFlow = _isOnline
+
init {
(context as? Application)?.registerActivityLifecycleCallbacks(callbacks)
}
diff --git a/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt b/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt
index fa6faca57d..dc1c3e9051 100644
--- a/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt
+++ b/core/datasource/src/main/java/com/tangem/datasource/connection/NetworkConnectionManager.kt
@@ -1,8 +1,13 @@
package com.tangem.datasource.connection
+import kotlinx.coroutines.flow.StateFlow
+
/** Network connection manager */
interface NetworkConnectionManager {
/** Connection status */
val isOnline: Boolean
+
+ /** Connection status flow */
+ val isOnlineFlow: StateFlow
}
\ No newline at end of file
diff --git a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json
index 26ee85d521..738e44b33c 100644
--- a/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json
+++ b/core/featuretoggles/src/main/assets/configs/feature_toggles_config.json
@@ -6,5 +6,9 @@
{
"name": "REDESIGNED_TOKEN_LIST_SCREEN_ENABLED",
"version": "4.4.0"
+ },
+ {
+ "name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED",
+ "version": "4.5.0"
}
]
\ No newline at end of file
diff --git a/core/res/src/main/res/values-de/strings.xml b/core/res/src/main/res/values-de/strings.xml
index f4ba8d958a..a2985232fd 100644
--- a/core/res/src/main/res/values-de/strings.xml
+++ b/core/res/src/main/res/values-de/strings.xml
@@ -1,283 +1,38 @@
- Add custom token
- Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.
- Request support
- Send feedback
Diese Karte wurde früher bereits aufgeladen und Transaktionen wurden damit signiert. Ziehen Sie eine sofortige Auszahlung aller Beträge in Betracht, wenn Sie diese Karte von einer nicht vertrauenswürdigen Quelle erhalten haben.
- This feature is disabled in Demo mode
- You are currently running in Demo mode. All funds are not real.
Die von Ihnen gescannte Karte ist eine Entwicklungskarte. Akzeptieren Sie sie nicht als Zahlungsmittel.
- Reason: %s
- Can\'t send a transaction
- Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.
- Tokens in Solana network are not supported by this card due to firmware limitation.
- This card is not a bearer note. We can\'t currently match the signature count on the card with the information on the blockchain. This is normal but in rare cases can mean a previous holder is holding back an offline signature, which is a security concern.\nDo not accept this card as physical payment from someone you don\'t trust.\nIt\'s perfectly safe in all other respects.\nTangem is the only hardware wallet to offer signature count protection.
- Are you having difficulty scanning your card?
Diese Karte ist für die Zusammenarbeit mit Tangem nicht geeignet
- Go to settings to enable biometric authentication in the Tangem App
- Enable biometric authentication
- This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code.
- Removing the saved card deletes all the saved wallets and their access codes from the app.
- Save Access Code
- Biometric authentication will be requested instead of the access code for interactions with your card.
- Keep the wallet in the app
- Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card.
- App Settings
- Please scan the card
- Please try again in 30 seconds or scan the card
- Too many attempts
- Reset
- Are you sure you want to do this?
- Change Access Code
- Access code will be changed on this card only
- Reset to Factory Settings
- Security Mode
- Card Settings
Tangem Bot
- Support
Akzeptieren
- Add
- Attention
Bilanz: %s
- biometric authentication
- biometrics
Sie haben keinen Zugang zur Kamera erteilt, bitte passen Sie Ihre Datenschutzeinstellungen an
Abbrechen
- Close
- Continue
- Copy
- Create
Entfernen
- Disconnect
Erledigt
- Enable
Fehler
- No
OK
- Primary Card
- Reject
- Retry
Änderungen speichern
- Search
- The server is not available, please try again later
- Share
- Sign
- Sign and send
- Start
- Submit
Erfolg
- terms and conditions
- I understand
Warnung
- Yes
- Contract address copied!
- Available networks
- Contract address
- Contract address is invalid
- Derivation path is invalid
- Please select the network
- Required field
- Decimal number must be a valid integer, no higher than %d
- Decimals
- Default
- BIP44 coin type
- E.g. USD Coin
- Name
- Not selected
- Network
- E.g. USDC
- Token symbol
- This token/network has already been added to your list
- Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing.
- Chat
Zugangscode
Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen.
Langes Tippen
Dieser Mechanismus schützt vor Annäherungsangriffen auf eine Karte. Es wird eine Verzögerung zwischen dem Empfang und der Ausführung eines Befehls erzwungen. Nach der ersten signierten Transaktion wird dieses Telefon mit der Karte verknüpft und die Transaktionen werden sofort signiert
Passcode
Bevor Sie einen Befehl ausführen, der eine Änderung des Kartenstatus zur Folge hat, müssen Sie den Passcode eingeben.
- Referral program
- Privacy policy
%s Hasch
- Card terms of use
KartenID
- Link More Cards
App Währung
Emittent
- Send Feedback
Signiert
Details
- Check your internet connection or switch to a different network
Nutzungsbedingungen
- Oops, the current version of the application is not ready to work with this card, please check for updates.
- You have used a card from another wallet. Tap the card associated with this wallet
- You Receive
- You Send
- The following information is optional. You can erase it if you don\'t want to share it.
- Tell us what functions you are missing, and we will try to help you.
- Please tell us what card do you have
- Hi support team,
- Please tell us more about your issue. Every small detail can help.
- My suggestions
- Can\'t scan a card
- Feedback
- Tangem feedback
- Can\'t send a transaction
- Order
Tippen Sie um den Zugangscode zu ändern
Tippen Sie um den Passcode zu ändern
- To create the wallet tap the card as shown above and do not remove until the end of the operation
Legen Sie die Karte zum Scannen an
Tippen um zu signieren
Legen Sie die Karte an
- Internal error: wallet manager not found
- Manage tokens
- To protect your assets, we advise you to carry out this procedure
- Your wallet has not been backed up
- Total balance
- The amount does not include some of your funds
- To access all the networks you need to scan the card
- Scan your card
- Tokens
- You have to set up a single access code to protect all your wallets
- Protect
- You can set up an individual access code on each card later
- Personalize
- The access code can be restored with a linked card, don\'t keep all cards in one place
- Restore
- Choose any word, phrase, or number you want as your access code
- Create Access Code
- Re-enter your Access Code
- Access code must be at least 4 characters long
- Entered access code didn\'t match the initial access code
- You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?
- The backup process is partly complete. You can\'t exit it now.
- Balance
- Add a backup card
- Scan the card #%d
- Backup now
- Scan the primary card
- Claim
- Continue to my wallet
- Finalize the backup process
- Verify via Utorg
- Refresh
- Set PIN code
- Receive crypto
- Register
- Scan primary card
- Skip for later
- How does it work?
- Let\'s generate all the keys on your card and create a secure wallet
- Create wallet
- Create a wallet
- Your card is activated and ready to be used
- Success!
- In this case, you will need to start from the beginning.
- Do you want to exit the activation process?
- Getting started
- Verify your identity
- KYC
- Pin code
- Connect
- Creating a backup
- Tap the SaltPay card
- To start the backup process you have to add the Tangem card as your backup
- Finalize the backup process by creating an access code
- Tap the Tangem card
- No backup card
- Backup card ready
- Prepare the SaltPay card
- To get started, simply claim wxDAI to your wallet
- It will take a few seconds
- Please check your email for further instructions
- To start using your card you have to pass the KYC process
- Please wait until the verification is completed. You\'ll be notified via email. Usually it takes up to 1 hour. You can close the app and come back later.
- To start the backup process add up to two backup cards.
- You can add one more card or finalize the backup process
- Set up a 4-digit code.\nIt will be used for payments.
- Connect your card to the decentralized payment system
- Prepare the backup card with number %s
- Prepare the primary card
- Prepare the primary card with number %s
- Congratulations! Your first payment crypto card has been activated!
- Your wallet card is configured and ready for use.
- Max number of cards added. Finalize the backup process.
- Activating card
- Backup card #%d
- Claim %s
- Claiming
- Something went wrong
- Verify your identity
- KYC is in progress
- No backup cards
- One backup card added
- PIN Code
- Connect your card
- Prepare your card
- Two backup cards added
- To get started, simply top up the wallet with any amount
- To get started, simply top up the wallet with more than %1$s %2$s
- Buy crypto
- Show the wallet\'s address
- Top up your wallet
- The twinning process is partly complete. You can\'t exit it now.
- If the process of creating the wallet gets interrupted in any way, you\'ll have to start over
- You can backup your keys up to two other blank Tangem Wallet cards.
- Access code can be restored with one of backup cards.
- All the backup cards can be used as full-functional with the identical keys.
- You will be able to set an access code to protect your wallets.
- Backup wallet
- Access code restore
- Identical cards
- Access code
- Participate
- Failed to load the information about the referral program. Please try again later.
- Failed to load the information about the referral program. Error code: %s. Please try again later.
- Your participation request could not be processed. Error code: %s. Please try again later. If the problem persists — feel free to contact our support.
- Your friends bought
- Will get
- for each wallet bought by your friend on your %1$s network address%2$s
- You
- Will get a
- when buying a card on tangem.com
- %s discount
- Your friend
- Personal code copied!
- Your personal code
- Buy Tangem Wallet with discount!\n%s
- Refer your friends to Tangem
- You\'ve accepted
- By tapping this button you accept
- of the referral program
- Please hold the card until the operation complete
- Reset the Card
- I understand that after performing this action, I will no longer have access to the current wallet
- Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.
- Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet.
- Do you have a bank card of another country or a UnionPay card?
- Russian bank cards are not accepted at the moment
- Tap the card with the visa logo
- Attention
- Please contact support
- No funds for activation
- Such a PIN can be brute-forced easily
- Four identical digits isn\'t safe
- Log into the app and check your balance without scanning the card
- Access the app
- Allow to use biometrics
- Biometrics will be requested instead of the access code for interactions with your wallet
- Access code
- It looks like you have biometric authentication disabled, it is necessary to save wallets
- Enable biometric authorization
- Would you like to use biometrics?
- Note that making a transaction with your funds will still require your card
- Scan card failed. Try again
- Scan Card
- Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.
- Get your card ready!
- Search tokens
Betrag
Adresse
Adresse oder PayString
@@ -285,8 +40,6 @@
PayString ist nicht registriert
PayString-Anfrage ist fehlgeschlagen
PayString wird von der Blockchain nicht unterstützt
- Invalid Tag. It won\'t be added to the transaction.
- Invalid Memo. It won\'t be added to the transaction.
Tag
Memo
inkl. Gebühr
@@ -297,141 +50,20 @@
Höchstbetrag
Netzgebühr
Absenden
- Sending %s
Gesamt
%1$s und %2$s werden gesendet
≈ %1$s (inkl. Gebühr: %2$s)
%s wird gesendet
Die Transaktion wurde erfolgreich signiert und an den Blockchain-Knoten gesendet. Die Walletbilanz wird aktualisiert
Ungültige Adresse
- Buy now
- Free
- I have a promo code…
Tangem Wallet
- Other payment methods
- Shipping
- Total
- Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.
- Store your crypto assets secure while keeping private keys contained in your card
- Revolutionary Hardware Wallet
- Up to
- 3 physical cards
- to one wallet
- Ultra Secure Backup
- A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card
- Thousands of Currencies
- Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto.
- The Wallet for Everyone
- Borrow
- Buy
- Exchange
- Lend
- Pay
- Send
- Store
- Meet\nTangem
- Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services
- DeFi Compatible
- Error: %s
- There was an error. Please try again.
- Give Permission
- High price impact!
- Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.
- Insufficient funds
- Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first.
- Transaction in progress…
- Waiting
- Approve
- Give Permission
- Amount %s
- Spender
- Your Wallet
- To continue you need to allow 1inch smart contracts to use your %s
- Permit and Swap
- View in Explorer
- In progress
- Swap
- Swap
- Swap of %s to
- Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.
- Other tokens
- Choose token
- Your tokens
- not available
- Hide
- You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.
- Hide %s
- Hide token
- %1$s is a token in the %2$s network. To make a %3$s transaction you need to deposit some %4$s (%5$s) to cover the network fee.
- Please wait for %s transaction to complete to be able to send funds
- The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.
- Unable to hide %s
- No rate
- You don\'t have any transactions yet
- Failed to load transactions
- Transactions
- In progress…
- You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d
- This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet.
- One wallet. Two cards.
- Scan the card #%s
- Creating wallet
- Scan the #%s twin card
- Preparing card
Tangem Twin
- This action is irreversible. You will not have access to the old wallet.
- Add new wallet
- Are you sure you want to delete this wallet?
- %d selected
- An error has occurred, please scan your card to log in
- This wallet has already been saved, you can add another one
- Multi-currency
- Wallet name
- Rename Wallet
- Single-currency
- My Wallets
- Unlock all with %s
PayString erstellen
- Adresse erkunden
- Network is unreachable
- Blockchain is unreachable. Try later
Die Bilanz wird aufgeladen…
- Scan the card
Die Transaktion läuft…
Verifizierte Bilanz
- Actions
- Buy
ein Wallet erstellen
- Sell
Absenden
- Exchange
- Do you want to buy or sell crypto?
- Requesting to sign a message.\n\n%s
- Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s
- Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s
- Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s
- Clipboard contain WalletConnect code. Use copied value or scan QR-code
- Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s
- Can\'t send transaction. Not enough funds.
- Failed to establish WalletConnect session. Please, try again later.
- Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n
- Failed to establish WalletConnect session: timeout error. Please, try again later.
- Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n
- Connection with this Dapp cannot be established due to its technical implementation.
- We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support
- %s network not found. Please, add it first and try again.
- No opened WalletConnect sessions
- Ooops. No Sessions.
- Open session
- Paste from clipboard
- Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s
- The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters.
- Scan new code
- This card can\'t be used to establish WalletConnect session
- This network is not supported. Please select another network.
- Select network
- WalletConnect Sessions
- Connect to Dapps
WalletConnect
PayString erstellen
$payid.tangem.com
@@ -440,11 +72,8 @@
Fehlerantwort beim Erstellen der PayString
PayString Name
PayString sind Ihre einzigartigen Identifikationsdaten wie Telefonnummer, E-Mail-Adresse oder ABN.
- %s network
Leere Karte
Erstellen Sie ein Wallet um die Tangem-Karte verwenden zu können
- Create twin wallet
- Generate wallet keys on both cards to start using your Twins
Das Konto ist nicht erstellt
Diese Karte wird nicht unterstützt
Ihre Tangem-Karte wurde für die Arbeit mit einer anderen App ausgestellt. Bitte sehen Sie sich den Namen und die Anleitung auf Ihrer Karte an und installieren Sie eine korrekte App
@@ -455,28 +84,5 @@
Senden
an %s
Tangem
- Can be better
- Learn more
OK, ich hab\'s!
- Really cool!
- %1$s network has a concept of Existential Deposit. If your account drops below %2$s it will be deactivated and any remaining funds will be destroyed.
- This card might be a production sample or counterfeit
- Authenticity check failed
- Important security information %s
- There are only %s signatures available on this card. You must withdraw all of your funds.
- How do you like Tangem?
- One question
- This card has signed transactions in the past
- This is a Testnet card. Don\'t accept it as a payment. This card must only be used for testing and development purposes.
- Discard
- You have an interrupted backup. Do you want to resume?
- Yes, resume
- Discard
- If you will discard the backup now, then you will have to reset the cards to factory settings to start over again
- Resume backup
- This is an irreversible action
- Log in with %s
- Scan card
- Use %s or scan a card to access the app
- Welcome back!
diff --git a/core/res/src/main/res/values-fr/strings.xml b/core/res/src/main/res/values-fr/strings.xml
index 8c367d88a4..fb6996f229 100644
--- a/core/res/src/main/res/values-fr/strings.xml
+++ b/core/res/src/main/res/values-fr/strings.xml
@@ -1,283 +1,38 @@
- Add custom token
- Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.
- Request support
- Send feedback
Cette carte a déjà été rechargée et a signé des transactions avant. Envisagez la possibilité de retirer tous les fonds immédiatement si vous avez reçu cette carte d\'une source non fiable.
- This feature is disabled in Demo mode
- You are currently running in Demo mode. All funds are not real.
La carte que vous avez scannée est une carte de développement. Ne l\'acceptez pas comme paiement.
- Reason: %s
- Can\'t send a transaction
- Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.
- Tokens in Solana network are not supported by this card due to firmware limitation.
- This card is not a bearer note. We can\'t currently match the signature count on the card with the information on the blockchain. This is normal but in rare cases can mean a previous holder is holding back an offline signature, which is a security concern.\nDo not accept this card as physical payment from someone you don\'t trust.\nIt\'s perfectly safe in all other respects.\nTangem is the only hardware wallet to offer signature count protection.
- Are you having difficulty scanning your card?
Cette carte n\'est pas conçue pour fonctionner avec Tangem
- Go to settings to enable biometric authentication in the Tangem App
- Enable biometric authentication
- This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code.
- Removing the saved card deletes all the saved wallets and their access codes from the app.
- Save Access Code
- Biometric authentication will be requested instead of the access code for interactions with your card.
- Keep the wallet in the app
- Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card.
- App Settings
- Please scan the card
- Please try again in 30 seconds or scan the card
- Too many attempts
- Reset
- Are you sure you want to do this?
- Change Access Code
- Access code will be changed on this card only
- Reset to Factory Settings
- Security Mode
- Card Settings
Tangem Bot
- Support
J\'accepte
- Add
- Attention
Solde : %s
- biometric authentication
- biometrics
Vous n\'avez pas octroyé l\'accès à votre caméra, veuillez modifier vos paramètres de confidentialité
Annuler
- Close
- Continue
- Copy
- Create
Supprimer
- Disconnect
Exécuté
- Enable
Erreur
- No
OK
- Primary Card
- Reject
- Retry
Sauvegarder les modifications
- Search
- The server is not available, please try again later
- Share
- Sign
- Sign and send
- Start
- Submit
Avec succès
- terms and conditions
- I understand
Alerte
- Yes
- Contract address copied!
- Available networks
- Contract address
- Contract address is invalid
- Derivation path is invalid
- Please select the network
- Required field
- Decimal number must be a valid integer, no higher than %d
- Decimals
- Default
- BIP44 coin type
- E.g. USD Coin
- Name
- Not selected
- Network
- E.g. USDC
- Token symbol
- This token/network has already been added to your list
- Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing.
- Chat
Code d\'accès
Vous devrez entrer le mot de passe correct avant de scanner la carte
Tenez la carte fermement
Ce mécanisme protège contre les attaques sans contact sur la carte. Il y a un délai entre la réception et l\'exécution de la commande. Après la première transaction signée, ce téléphone sera associé à la carte et les transactions seront signées immédiatement.
Mot de passe
Avant d\'exécuter une commande qui modifie l\'état de la carte, vous devrez entrer un mot de passe.
- Referral program
- Privacy policy
%s hashes
- Card terms of use
ID de la carte
- Link More Cards
Monnaie de l\'application
Emetteur
- Send Feedback
Signé
Référénces
- Check your internet connection or switch to a different network
Conditions d\'utilisation
- Oops, the current version of the application is not ready to work with this card, please check for updates.
- You have used a card from another wallet. Tap the card associated with this wallet
- You Receive
- You Send
- The following information is optional. You can erase it if you don\'t want to share it.
- Tell us what functions you are missing, and we will try to help you.
- Please tell us what card do you have
- Hi support team,
- Please tell us more about your issue. Every small detail can help.
- My suggestions
- Can\'t scan a card
- Feedback
- Tangem feedback
- Can\'t send a transaction
- Order
Touchez, pour modifier le code d\'accès
Touchez, pour modifier le mot de passe
- To create the wallet tap the card as shown above and do not remove until the end of the operation
Posez pour scanner
Touchez pour signer
Posez la carte
- Internal error: wallet manager not found
- Manage tokens
- To protect your assets, we advise you to carry out this procedure
- Your wallet has not been backed up
- Total balance
- The amount does not include some of your funds
- To access all the networks you need to scan the card
- Scan your card
- Tokens
- You have to set up a single access code to protect all your wallets
- Protect
- You can set up an individual access code on each card later
- Personalize
- The access code can be restored with a linked card, don\'t keep all cards in one place
- Restore
- Choose any word, phrase, or number you want as your access code
- Create Access Code
- Re-enter your Access Code
- Access code must be at least 4 characters long
- Entered access code didn\'t match the initial access code
- You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?
- The backup process is partly complete. You can\'t exit it now.
- Balance
- Add a backup card
- Scan the card #%d
- Backup now
- Scan the primary card
- Claim
- Continue to my wallet
- Finalize the backup process
- Verify via Utorg
- Refresh
- Set PIN code
- Receive crypto
- Register
- Scan primary card
- Skip for later
- How does it work?
- Let\'s generate all the keys on your card and create a secure wallet
- Create wallet
- Create a wallet
- Your card is activated and ready to be used
- Success!
- In this case, you will need to start from the beginning.
- Do you want to exit the activation process?
- Getting started
- Verify your identity
- KYC
- Pin code
- Connect
- Creating a backup
- Tap the SaltPay card
- To start the backup process you have to add the Tangem card as your backup
- Finalize the backup process by creating an access code
- Tap the Tangem card
- No backup card
- Backup card ready
- Prepare the SaltPay card
- To get started, simply claim wxDAI to your wallet
- It will take a few seconds
- Please check your email for further instructions
- To start using your card you have to pass the KYC process
- Please wait until the verification is completed. You\'ll be notified via email. Usually it takes up to 1 hour. You can close the app and come back later.
- To start the backup process add up to two backup cards.
- You can add one more card or finalize the backup process
- Set up a 4-digit code.\nIt will be used for payments.
- Connect your card to the decentralized payment system
- Prepare the backup card with number %s
- Prepare the primary card
- Prepare the primary card with number %s
- Congratulations! Your first payment crypto card has been activated!
- Your wallet card is configured and ready for use.
- Max number of cards added. Finalize the backup process.
- Activating card
- Backup card #%d
- Claim %s
- Claiming
- Something went wrong
- Verify your identity
- KYC is in progress
- No backup cards
- One backup card added
- PIN Code
- Connect your card
- Prepare your card
- Two backup cards added
- To get started, simply top up the wallet with any amount
- To get started, simply top up the wallet with more than %1$s %2$s
- Buy crypto
- Show the wallet\'s address
- Top up your wallet
- The twinning process is partly complete. You can\'t exit it now.
- If the process of creating the wallet gets interrupted in any way, you\'ll have to start over
- You can backup your keys up to two other blank Tangem Wallet cards.
- Access code can be restored with one of backup cards.
- All the backup cards can be used as full-functional with the identical keys.
- You will be able to set an access code to protect your wallets.
- Backup wallet
- Access code restore
- Identical cards
- Access code
- Participate
- Failed to load the information about the referral program. Please try again later.
- Failed to load the information about the referral program. Error code: %s. Please try again later.
- Your participation request could not be processed. Error code: %s. Please try again later. If the problem persists — feel free to contact our support.
- Your friends bought
- Will get
- for each wallet bought by your friend on your %1$s network address%2$s
- You
- Will get a
- when buying a card on tangem.com
- %s discount
- Your friend
- Personal code copied!
- Your personal code
- Buy Tangem Wallet with discount!\n%s
- Refer your friends to Tangem
- You\'ve accepted
- By tapping this button you accept
- of the referral program
- Please hold the card until the operation complete
- Reset the Card
- I understand that after performing this action, I will no longer have access to the current wallet
- Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.
- Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet.
- Do you have a bank card of another country or a UnionPay card?
- Russian bank cards are not accepted at the moment
- Tap the card with the visa logo
- Attention
- Please contact support
- No funds for activation
- Such a PIN can be brute-forced easily
- Four identical digits isn\'t safe
- Log into the app and check your balance without scanning the card
- Access the app
- Allow to use biometrics
- Biometrics will be requested instead of the access code for interactions with your wallet
- Access code
- It looks like you have biometric authentication disabled, it is necessary to save wallets
- Enable biometric authorization
- Would you like to use biometrics?
- Note that making a transaction with your funds will still require your card
- Scan card failed. Try again
- Scan Card
- Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.
- Get your card ready!
- Search tokens
Somme
Adresse
Adresse ou PayString
@@ -285,8 +40,6 @@
PayString non enregistré
La demande de PayString a échoué
PayString non pris en charge par la blockchain
- Invalid Tag. It won\'t be added to the transaction.
- Invalid Memo. It won\'t be added to the transaction.
Tag
Memo
Inclure les commissions
@@ -297,141 +50,20 @@
Somme maximale
Commissions du réseau
Envoyer
- Sending %s
Total
Sera envoyé %1$s et %2$s
≈ %1$s (incl. les commissions : %2$s)
Sera envoyé %s
La transaction a été signée avec succès et envoyée au nœud de blockchain. Le solde du portefeuille sera mis à jour après un certain temps
Adresse incorrecte
- Buy now
- Free
- I have a promo code…
Tangem Wallet
- Other payment methods
- Shipping
- Total
- Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.
- Store your crypto assets secure while keeping private keys contained in your card
- Revolutionary Hardware Wallet
- Up to
- 3 physical cards
- to one wallet
- Ultra Secure Backup
- A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card
- Thousands of Currencies
- Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto.
- The Wallet for Everyone
- Borrow
- Buy
- Exchange
- Lend
- Pay
- Send
- Store
- Meet\nTangem
- Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services
- DeFi Compatible
- Error: %s
- There was an error. Please try again.
- Give Permission
- High price impact!
- Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.
- Insufficient funds
- Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first.
- Transaction in progress…
- Waiting
- Approve
- Give Permission
- Amount %s
- Spender
- Your Wallet
- To continue you need to allow 1inch smart contracts to use your %s
- Permit and Swap
- View in Explorer
- In progress
- Swap
- Swap
- Swap of %s to
- Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.
- Other tokens
- Choose token
- Your tokens
- not available
- Hide
- You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.
- Hide %s
- Hide token
- %1$s is a token in the %2$s network. To make a %3$s transaction you need to deposit some %4$s (%5$s) to cover the network fee.
- Please wait for %s transaction to complete to be able to send funds
- The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.
- Unable to hide %s
- No rate
- You don\'t have any transactions yet
- Failed to load transactions
- Transactions
- In progress…
- You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d
- This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet.
- One wallet. Two cards.
- Scan the card #%s
- Creating wallet
- Scan the #%s twin card
- Preparing card
Tangem Twin
- This action is irreversible. You will not have access to the old wallet.
- Add new wallet
- Are you sure you want to delete this wallet?
- %d selected
- An error has occurred, please scan your card to log in
- This wallet has already been saved, you can add another one
- Multi-currency
- Wallet name
- Rename Wallet
- Single-currency
- My Wallets
- Unlock all with %s
Créer PayString
- Explorer l\'adresse
- Network is unreachable
- Blockchain is unreachable. Try later
Solde est en cours de téléchargement…
- Scan the card
Transaction en cours…
Solde confirmé
- Actions
- Buy
Créer un portefeuille
- Sell
Envoyer
- Exchange
- Do you want to buy or sell crypto?
- Requesting to sign a message.\n\n%s
- Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s
- Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s
- Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s
- Clipboard contain WalletConnect code. Use copied value or scan QR-code
- Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s
- Can\'t send transaction. Not enough funds.
- Failed to establish WalletConnect session. Please, try again later.
- Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n
- Failed to establish WalletConnect session: timeout error. Please, try again later.
- Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n
- Connection with this Dapp cannot be established due to its technical implementation.
- We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support
- %s network not found. Please, add it first and try again.
- No opened WalletConnect sessions
- Ooops. No Sessions.
- Open session
- Paste from clipboard
- Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s
- The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters.
- Scan new code
- This card can\'t be used to establish WalletConnect session
- This network is not supported. Please select another network.
- Select network
- WalletConnect Sessions
- Connect to Dapps
WalletConnect
Créer PayString
$payid.tangem.com
@@ -440,11 +72,8 @@
Mauvaise réponse lors de la création de PayString
Dénomination PayString
Votre PayString est une information qui vous est propre, comme un numéro de téléphone, une adresse du courrier électronique ou un ABN.
- %s network
Carte vide
Créez un portefeuille pour commencer à utiliser votre carte Tangem
- Create twin wallet
- Generate wallet keys on both cards to start using your Twins
Compte n\'est pas créé
Cette carte n\'est pas prise en charge
Votre carte Tangem a été créée pour fonctionner avec une autre application. Regardez le nom et les instructions sur votre carte et installez l\'application pertinente
@@ -455,28 +84,5 @@
Envoi
jusqu\'à %s
Tangem
- Can be better
- Learn more
Ok, je l\'ai!
- Really cool!
- %1$s network has a concept of Existential Deposit. If your account drops below %2$s it will be deactivated and any remaining funds will be destroyed.
- This card might be a production sample or counterfeit
- Authenticity check failed
- Important security information %s
- There are only %s signatures available on this card. You must withdraw all of your funds.
- How do you like Tangem?
- One question
- This card has signed transactions in the past
- This is a Testnet card. Don\'t accept it as a payment. This card must only be used for testing and development purposes.
- Discard
- You have an interrupted backup. Do you want to resume?
- Yes, resume
- Discard
- If you will discard the backup now, then you will have to reset the cards to factory settings to start over again
- Resume backup
- This is an irreversible action
- Log in with %s
- Scan card
- Use %s or scan a card to access the app
- Welcome back!
diff --git a/core/res/src/main/res/values-it/strings.xml b/core/res/src/main/res/values-it/strings.xml
index 53671cbc35..f1168f53e3 100644
--- a/core/res/src/main/res/values-it/strings.xml
+++ b/core/res/src/main/res/values-it/strings.xml
@@ -1,283 +1,38 @@
- Add custom token
- Send only %1$s (%2$s) from %3$s network to this address. Using other tokens and networks may result in loss of funds.
- Request support
- Send feedback
Questa carta è già stata ricaricata e ha firmato transazioni in passato. Valuta la possibilità di prelevare immediatamente tutti i fondi se hai ricevuto questa carta da una fonte inaffidabile.
- This feature is disabled in Demo mode
- You are currently running in Demo mode. All funds are not real.
La carta che hai scansionato è una carta di sviluppo. Non utilizzarla come strumento di pagamento
- Reason: %s
- Can\'t send a transaction
- Note that tokens on different networks have different addresses. Double check that your address matches the network when you transfer funds.
- Tokens in Solana network are not supported by this card due to firmware limitation.
- This card is not a bearer note. We can\'t currently match the signature count on the card with the information on the blockchain. This is normal but in rare cases can mean a previous holder is holding back an offline signature, which is a security concern.\nDo not accept this card as physical payment from someone you don\'t trust.\nIt\'s perfectly safe in all other respects.\nTangem is the only hardware wallet to offer signature count protection.
- Are you having difficulty scanning your card?
Questa carta non è progettata per funzionare con Tangem
- Go to settings to enable biometric authentication in the Tangem App
- Enable biometric authentication
- This will delete all the saved wallet access codes. Any further operation with the wallet will require submitting the access code.
- Removing the saved card deletes all the saved wallets and their access codes from the app.
- Save Access Code
- Biometric authentication will be requested instead of the access code for interactions with your card.
- Keep the wallet in the app
- Enable to link all the wallets to Tangem app. Biometric authentication will be required for unlocking the app. Transaction signing requires tapping your Tangem card.
- App Settings
- Please scan the card
- Please try again in 30 seconds or scan the card
- Too many attempts
- Reset
- Are you sure you want to do this?
- Change Access Code
- Access code will be changed on this card only
- Reset to Factory Settings
- Security Mode
- Card Settings
Tangem Bot
- Support
Accetta
- Add
- Attention
Saldo: %s
- biometric authentication
- biometrics
Non hai fornito l\'accesso alla tua videocamera, modifica le tue impostazioni sulla privacy
Annulla
- Close
- Continue
- Copy
- Create
Rimuovere
- Disconnect
Fatto
- Enable
Errore
- No
OK
- Primary Card
- Reject
- Retry
Mantieni le modifiche
- Search
- The server is not available, please try again later
- Share
- Sign
- Sign and send
- Start
- Submit
Con successo
- terms and conditions
- I understand
Avviso
- Yes
- Contract address copied!
- Available networks
- Contract address
- Contract address is invalid
- Derivation path is invalid
- Please select the network
- Required field
- Decimal number must be a valid integer, no higher than %d
- Decimals
- Default
- BIP44 coin type
- E.g. USD Coin
- Name
- Not selected
- Network
- E.g. USDC
- Token symbol
- This token/network has already been added to your list
- Note that tokens can be created by anyone. Be aware of adding scam tokens, they can cost nothing.
- Chat
Codice di accesso
Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto
Mantenimento della carta
Questo meccanismo protegge dagli avvicinamenti senza contatto sulla carta. Attiva un ritardo tra la ricezione e l\'esecuzione di un comando. Dopo la prima transazione firmata, questo telefono verrà associato alla carta e le transazioni verranno firmate immediatamente.
Password
Dovrai inserire una password prima di eseguire qualsiasi comando che modifichi lo stato della carta.
- Referral program
- Privacy policy
Hash %s
- Card terms of use
ID carta
- Link More Cards
Valuta dell\'applicazione
Emittente
- Send Feedback
Firmato
Requisiti
- Check your internet connection or switch to a different network
Termini del servizio
- Oops, the current version of the application is not ready to work with this card, please check for updates.
- You have used a card from another wallet. Tap the card associated with this wallet
- You Receive
- You Send
- The following information is optional. You can erase it if you don\'t want to share it.
- Tell us what functions you are missing, and we will try to help you.
- Please tell us what card do you have
- Hi support team,
- Please tell us more about your issue. Every small detail can help.
- My suggestions
- Can\'t scan a card
- Feedback
- Tangem feedback
- Can\'t send a transaction
- Order
Avvicina per modificare il codice di accesso
Avvicina per modificare la password
- To create the wallet tap the card as shown above and do not remove until the end of the operation
Avvicina per scansionare
Avvicina per firmare
Avvicina la carta
- Internal error: wallet manager not found
- Manage tokens
- To protect your assets, we advise you to carry out this procedure
- Your wallet has not been backed up
- Total balance
- The amount does not include some of your funds
- To access all the networks you need to scan the card
- Scan your card
- Tokens
- You have to set up a single access code to protect all your wallets
- Protect
- You can set up an individual access code on each card later
- Personalize
- The access code can be restored with a linked card, don\'t keep all cards in one place
- Restore
- Choose any word, phrase, or number you want as your access code
- Create Access Code
- Re-enter your Access Code
- Access code must be at least 4 characters long
- Entered access code didn\'t match the initial access code
- You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?
- The backup process is partly complete. You can\'t exit it now.
- Balance
- Add a backup card
- Scan the card #%d
- Backup now
- Scan the primary card
- Claim
- Continue to my wallet
- Finalize the backup process
- Verify via Utorg
- Refresh
- Set PIN code
- Receive crypto
- Register
- Scan primary card
- Skip for later
- How does it work?
- Let\'s generate all the keys on your card and create a secure wallet
- Create wallet
- Create a wallet
- Your card is activated and ready to be used
- Success!
- In this case, you will need to start from the beginning.
- Do you want to exit the activation process?
- Getting started
- Verify your identity
- KYC
- Pin code
- Connect
- Creating a backup
- Tap the SaltPay card
- To start the backup process you have to add the Tangem card as your backup
- Finalize the backup process by creating an access code
- Tap the Tangem card
- No backup card
- Backup card ready
- Prepare the SaltPay card
- To get started, simply claim wxDAI to your wallet
- It will take a few seconds
- Please check your email for further instructions
- To start using your card you have to pass the KYC process
- Please wait until the verification is completed. You\'ll be notified via email. Usually it takes up to 1 hour. You can close the app and come back later.
- To start the backup process add up to two backup cards.
- You can add one more card or finalize the backup process
- Set up a 4-digit code.\nIt will be used for payments.
- Connect your card to the decentralized payment system
- Prepare the backup card with number %s
- Prepare the primary card
- Prepare the primary card with number %s
- Congratulations! Your first payment crypto card has been activated!
- Your wallet card is configured and ready for use.
- Max number of cards added. Finalize the backup process.
- Activating card
- Backup card #%d
- Claim %s
- Claiming
- Something went wrong
- Verify your identity
- KYC is in progress
- No backup cards
- One backup card added
- PIN Code
- Connect your card
- Prepare your card
- Two backup cards added
- To get started, simply top up the wallet with any amount
- To get started, simply top up the wallet with more than %1$s %2$s
- Buy crypto
- Show the wallet\'s address
- Top up your wallet
- The twinning process is partly complete. You can\'t exit it now.
- If the process of creating the wallet gets interrupted in any way, you\'ll have to start over
- You can backup your keys up to two other blank Tangem Wallet cards.
- Access code can be restored with one of backup cards.
- All the backup cards can be used as full-functional with the identical keys.
- You will be able to set an access code to protect your wallets.
- Backup wallet
- Access code restore
- Identical cards
- Access code
- Participate
- Failed to load the information about the referral program. Please try again later.
- Failed to load the information about the referral program. Error code: %s. Please try again later.
- Your participation request could not be processed. Error code: %s. Please try again later. If the problem persists — feel free to contact our support.
- Your friends bought
- Will get
- for each wallet bought by your friend on your %1$s network address%2$s
- You
- Will get a
- when buying a card on tangem.com
- %s discount
- Your friend
- Personal code copied!
- Your personal code
- Buy Tangem Wallet with discount!\n%s
- Refer your friends to Tangem
- You\'ve accepted
- By tapping this button you accept
- of the referral program
- Please hold the card until the operation complete
- Reset the Card
- I understand that after performing this action, I will no longer have access to the current wallet
- Factory Reset will completely delete the wallet from the selected card. You will not be able to restore the current wallet or use the card to recover the access code.
- Factory Reset will completely delete the wallet from the selected card and remove it from the app. You will not be able to restore the current wallet.
- Do you have a bank card of another country or a UnionPay card?
- Russian bank cards are not accepted at the moment
- Tap the card with the visa logo
- Attention
- Please contact support
- No funds for activation
- Such a PIN can be brute-forced easily
- Four identical digits isn\'t safe
- Log into the app and check your balance without scanning the card
- Access the app
- Allow to use biometrics
- Biometrics will be requested instead of the access code for interactions with your wallet
- Access code
- It looks like you have biometric authentication disabled, it is necessary to save wallets
- Enable biometric authorization
- Would you like to use biometrics?
- Note that making a transaction with your funds will still require your card
- Scan card failed. Try again
- Scan Card
- Scan the card to change its settings. The changes will impact only the card you\'ve scanned and will not affect other cards tied to your wallet.
- Get your card ready!
- Search tokens
Importo
Indirizzo
Indirizzo o PayString
@@ -285,8 +40,6 @@
PayString non registrato
Richiesta PayString fallita
PayString non supportato dalla blockchain
- Invalid Tag. It won\'t be added to the transaction.
- Invalid Memo. It won\'t be added to the transaction.
Tag
Memo
Includi commissione
@@ -297,141 +50,20 @@
Importo totale
Costi della rete
Invia
- Sending %s
Totale
Sarà inviato %1$s e %2$s
≈ %1$s (inc. commissione: %2$s)
Sarà inviato %s
La transazione è stata firmata con successo e inviata al nodo blockchain. Il saldo del portafoglio verrà aggiornato dopo un po\' di tempo
Indirizzo non valido
- Buy now
- Free
- I have a promo code…
Tangem Wallet
- Other payment methods
- Shipping
- Total
- Solana network charges a rent of %1$s every 2 days. Accounts that can\'t afford the rent are purged from the network. Deposit your account with more than %2$s to use it for free.
- Store your crypto assets secure while keeping private keys contained in your card
- Revolutionary Hardware Wallet
- Up to
- 3 physical cards
- to one wallet
- Ultra Secure Backup
- A hardware wallet for your Bitcoin, Ethereum and many more currencies simultaneously – all in one card
- Thousands of Currencies
- Use it on the go, anywhere, anytime. No wires or batteries. Just tap the card to your phone when you need your crypto.
- The Wallet for Everyone
- Borrow
- Buy
- Exchange
- Lend
- Pay
- Send
- Store
- Meet\nTangem
- Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services
- DeFi Compatible
- Error: %s
- There was an error. Please try again.
- Give Permission
- High price impact!
- Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.
- Insufficient funds
- Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first.
- Transaction in progress…
- Waiting
- Approve
- Give Permission
- Amount %s
- Spender
- Your Wallet
- To continue you need to allow 1inch smart contracts to use your %s
- Permit and Swap
- View in Explorer
- In progress
- Swap
- Swap
- Swap of %s to
- Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.
- Other tokens
- Choose token
- Your tokens
- not available
- Hide
- You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.
- Hide %s
- Hide token
- %1$s is a token in the %2$s network. To make a %3$s transaction you need to deposit some %4$s (%5$s) to cover the network fee.
- Please wait for %s transaction to complete to be able to send funds
- The %1$s token is the main currency on the %2$s network and cannot be hidden as long as you have other tokens on this network in the list.
- Unable to hide %s
- No rate
- You don\'t have any transactions yet
- Failed to load transactions
- Transactions
- In progress…
- You\'ve scanned the same card. To create a twin wallet you need to scan the card with number %d
- This one that you are holding in your hands and the other one with number %s.\n\nBoth cards can be used to extract funds from this wallet.
- One wallet. Two cards.
- Scan the card #%s
- Creating wallet
- Scan the #%s twin card
- Preparing card
Tangem Twin
- This action is irreversible. You will not have access to the old wallet.
- Add new wallet
- Are you sure you want to delete this wallet?
- %d selected
- An error has occurred, please scan your card to log in
- This wallet has already been saved, you can add another one
- Multi-currency
- Wallet name
- Rename Wallet
- Single-currency
- My Wallets
- Unlock all with %s
Crea PayString
- Cerca indirizzo
- Network is unreachable
- Blockchain is unreachable. Try later
Il saldo sta per essere caricato…
- Scan the card
Transazione in corso…
Saldo verificato
- Actions
- Buy
Crea portafoglio
- Sell
Invia
- Exchange
- Do you want to buy or sell crypto?
- Requesting to sign a message.\n\n%s
- Dapp %1$s, requesting to\nsign BNB transaction.\n\n%2$s
- Trade order for %1$s\nPrice: %2$s\nAmount to receive: %3$s\nAmount to pay: %4$s
- Transaction details:\nFrom: %1$s\nTo: %2$s\nAmount: %3$s
- Clipboard contain WalletConnect code. Use copied value or scan QR-code
- Request to create transaction for %1$s\n%2$s\n\nAmount: %3$s\nFee: %4$s\nTotal: %5$s\nBalance: %6$s
- Can\'t send transaction. Not enough funds.
- Failed to establish WalletConnect session. Please, try again later.
- Not all tokens were added to your list. Please add them first and try again. Missing tokens:\n
- Failed to establish WalletConnect session: timeout error. Please, try again later.
- Session request contains unsupported blockchains for WalletConnect connection. Unsupported blockchains:\n
- Connection with this Dapp cannot be established due to its technical implementation.
- We\'ve encountered unknown error. Error code: %d. If the problem persists — feel free to contact our support
- %s network not found. Please, add it first and try again.
- No opened WalletConnect sessions
- Ooops. No Sessions.
- Open session
- Paste from clipboard
- Request to start a session for\n%1$s\n\nNETWORK: %2$s\n\nURL: %3$s
- The operation couldn\'t be completed.\n\nYou have already established a WalletConnect session with this parameters.
- Scan new code
- This card can\'t be used to establish WalletConnect session
- This network is not supported. Please select another network.
- Select network
- WalletConnect Sessions
- Connect to Dapps
WalletConnect
Crea PayString
$payid.tangem.com
@@ -440,11 +72,8 @@
Errore di risposta durante la creazione del PayString
Nome PayString
Il tuo PayString – informazioni per te uniche, come il tuo numero di telefono, indirizzo email o ABN.
- %s network
Carta vuota
Crea un portafoglio per iniziare ad utilizzare la tua carta Tangem
- Create twin wallet
- Generate wallet keys on both cards to start using your Twins
Conto non creato
Questa carta non è supportata
La tua carta Tangem è stata creata per funzionare con un\'altra applicazione. Leggere il nome e le istruzioni sulla carta e installare l\'applicazione corretta
@@ -455,28 +84,5 @@
Inviau
su %s
Tangem
- Can be better
- Learn more
Ok, ho capito!
- Really cool!
- %1$s network has a concept of Existential Deposit. If your account drops below %2$s it will be deactivated and any remaining funds will be destroyed.
- This card might be a production sample or counterfeit
- Authenticity check failed
- Important security information %s
- There are only %s signatures available on this card. You must withdraw all of your funds.
- How do you like Tangem?
- One question
- This card has signed transactions in the past
- This is a Testnet card. Don\'t accept it as a payment. This card must only be used for testing and development purposes.
- Discard
- You have an interrupted backup. Do you want to resume?
- Yes, resume
- Discard
- If you will discard the backup now, then you will have to reset the cards to factory settings to start over again
- Resume backup
- This is an irreversible action
- Log in with %s
- Scan card
- Use %s or scan a card to access the app
- Welcome back!
diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml
index f11b1ab441..e27b0df710 100644
--- a/core/res/src/main/res/values-ru/strings.xml
+++ b/core/res/src/main/res/values-ru/strings.xml
@@ -27,6 +27,11 @@
Пожалуйста, отсканируйте карту
Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту
Слишком много попыток
+ Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.
+ Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте.
+ Использовать эту карту для сброса кода доступа на других картах в этом кошельке
+ Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька
+ Восстановление кода доступа
Сбросить
Вы уверены, что хотите это сделать?
Смена кода доступа
@@ -36,6 +41,10 @@
Настройки карты
Tangem Bot
Чат
+ Оценить оператора
+ Отправить логи
+ Пожалуйста, выберите действие
+ Пожалуйста, оцените работу оператора
Принять
Добавить
Внимание
@@ -49,10 +58,15 @@
Копировать
Создать
Удалить
+ Отключено
Отключить
+ Не нравится
Готово
Включить
+ Включено
Ошибка
+ Импортировать
+ Нравится
Нет
Ок
Основная карта
@@ -173,11 +187,15 @@
Давайте сгенерируем все ключи на вашей карте и создадим безопасный кошелек
Создать кошелек
Создать кошелек
+ Другие опции
+ Ваши ключи будут надежно сгенерированы внутри карты. Никакой seed-фразы, а это значит, что никто не может экспортировать или украсть ее.
+ Cоздавайте ключи приватно
Ваша карта активирована и готова к использованию
Успешно!
В этом случае вам будет необходимо начать процесс заново.
Вы хотите выйти из процесса активации?
Подготовка
+ Другой кошелек уже был создан на карте, которую вы пытаетесь добавить. Хотите сбросить его и использовать карту для бэкапа?
Подтвердите свою личность
Верификация клиента
Код доступа
@@ -190,6 +208,20 @@
Бэкап карта не добавлена
Бэкап карта создана
Приготовьте SaltPay карту
+ Читать про секретную фразу
+ Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте.
+ Ваша секретная фраза
+ Чтобы импортировать кошелек, введите секретную фразу в поле ниже
+ Cоздать секретную фразу
+ Импорт кошелька
+ Секретная фраза — это набор слов, который дает возможность восстановить кошелек. В отличие от ключей, сгенерированных картой, seed-фраза не защищена и может быть скопирована и украдена. Используйте этот вариант на свой страх и риск.
+ Использовать секретную фразу
+ Неверная секретная фраза. Пожалуйста, проверьте порядок слов.
+ Неверная секретная фраза. Пожалуйста, проверьте орфографию.
+ Устаревший
+ Мы не рекомендуем хранить секретную фразу в виде скриншота из-за высокого риска утери или взлома
+ Чтобы проверить, правильно ли вы записали секретную фразу, введите 2-е, 7-е и 11-е слова
+ Итак, проверим
Для начала работы просто запросите начисление wxDAI на свой кошелек
Это займет несколько секунд
Более подробная информация отправлена на ваш адрес электронной почты.
diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml
index 4758954e64..648337e245 100644
--- a/core/res/src/main/res/values-zh-rTW/strings.xml
+++ b/core/res/src/main/res/values-zh-rTW/strings.xml
@@ -27,6 +27,10 @@
請掃描卡片
請30秒後重試或刷卡
嘗試次數過多
+ 您將無法使用此卡重置訪問密碼
+ 您將能夠使用此卡重置訪問密碼
+ 啟用使用此卡重置您的錢包訪問密碼
+ 恢復訪問密碼
重置
您確定要這麼做嗎?
更改訪問密碼
@@ -49,10 +53,13 @@
複製
創造
刪除
+ 禁用
斷開連接
完成
允許
+ 啟用
錯誤
+ 導入
否
OK
主卡片
@@ -107,7 +114,7 @@
發行人
發送反饋
簽署
- 細節
+ 更多
檢查您的網路連接或切換到其他網絡
服務條款
糟糕,當前版本的應用程序無法使用此卡,請檢查更新
@@ -118,7 +125,7 @@
告訴我們您缺少哪些功能,我們會盡力幫助您
請告訴我們你有什麼卡
嗨! 幫助團隊,
- 請告訴我們更多有關您的問題的信息。提供任何小細節都會帶來幫助
+ 請告訴我們更多有關您的問題。提供任何細節都會帶來幫助
我的建議
無法掃卡
反饋
@@ -132,6 +139,7 @@
點擊簽名
點按卡片
內部錯誤:找不到錢包管理器
+ 您已更新生物識別登入,掃描您的卡進入
管理代幣
為了保護您的資產,我們建議您執行此程序
您的錢包尚未備份
@@ -172,6 +180,9 @@
讓我們生成您卡上的所有密鑰並創建一個安全的錢包
創建錢包
創造錢包
+ 其他選項
+ 您的密鑰將在卡內安全生成。沒有種子短語,這意味著沒有人可以導出或竊取它。
+ 私下生成密鑰
您的卡已激活並可以使用
成功!
這此情況,您必須要重新開始
@@ -189,6 +200,20 @@
沒有備份卡片
備份卡片已準備完成
準備SaltPay卡
+ 閱讀更多關於助記詞的訊息
+ 按照下面給出的順序寫下這 12 個單詞,並將它們存放在一個隱密、安全的地方
+ 您的助記詞
+ 要導入您的錢包,請在下面的字段中輸入您的秘密助記詞
+ 生成助記詞
+ 導入錢包
+ 註記詞是一系列能夠恢復錢包的單詞。與卡片生成的密鑰不同,助記詞不受保護,可以被複製和竊取。使用此選項需要您自擔風險。
+ 使用助記詞
+ 無效的助記詞。請檢查單詞順序。
+ 無效的助記詞。請檢查您的拼寫。
+ Legacy
+ 由於丟失或被駭客入侵的風險很高,我們不建議將密碼截圖
+ 為了檢查您是否正確輸入了助記詞,請輸入第 2、7 和 11 個單詞
+ 那麼,讓我們檢查一下
要開始,只需將 wxDAI 獲取到您的錢包
這會花上幾秒
請查看Email已獲得更多指示
@@ -224,9 +249,9 @@
充值你的錢包
結對過程已部分完成。你現在不能退出
如果創建錢包的過程以任何方式中斷,您將得重新開始
- 您最多可以備份另外兩張空白的 Tangem 錢包卡。
+ 您最多可以額外備份兩張空白的 Tangem冷錢包
可以使用其中一張備用卡恢復訪問密碼
- 所有備用卡都可以使用相同的密鑰作為全功能使用。
+ 所有備用卡都可以使用相同的密鑰作為全功能使用
您將能夠設置訪問密碼來保護您的錢包
備援錢包
訪問密碼還原
@@ -238,7 +263,7 @@
無法處理您的請求。原因:%s。請稍後再試。無如果問題仍然存在—請隨時聯繫服務人員
您的朋友買
會得到
- 對於你的朋友在你的 %1$s 網絡地址%2$s 上購買的每個錢包
+ 對於你的朋友在你的 %1$s 網絡地址%2$s上購買的每個錢包
你
得到
當在 tangem.com購買卡片
@@ -335,11 +360,11 @@
錯誤: %s
有錯誤。請再試一遍
賦予權限
- High price impact!
- Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.
+ 價格影響高!
+ 在此代幣交換的數量將對價格產生重大影響,並降低您收到的數量
餘額不足
您的 %1$s 錢包中沒有足夠的資金來創建交易。首先為您的 %2$s 錢包充值
- 交易進行中…
+ 交易進行中...
等待中
允許
賦予權限
@@ -351,9 +376,9 @@
在瀏覽器中查看
進行中
交換
- Swap
+ 交易
交易 %s 至
- Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.
+ 此外,報價包括%s的 Tangem 費用。這有助於我們提供一流的產品
其他代幣
選擇代幣
您的代幣
@@ -383,7 +408,7 @@
添加新錢包
您確定要刪除此錢包?
已選擇 %d
- An error has occurred, please scan your card to log in
+ 發生錯誤,請掃卡登錄
此錢包已保存,您可以再添加一個
多幣種
錢包名稱
@@ -418,9 +443,10 @@
無法建立 WalletConnect 連接:超時錯誤。請稍後再試
會話請求包含不支持 WalletConnect 連接的區塊鏈。不支持的區塊鏈:\n
由於技術問題,無法與此 Dapp 建立連接
+ 在 Tangem App 中選擇了錯誤的卡
我們遇到了未知錯誤。錯誤代碼:%d。如果問題仍然存在-請隨時聯繫我們的支持人員
沒有 %s 網路,請先加入後再試一次
- 沒有打開中的WalletConnect連接
+ 沒有已連結的WalletConnect
Ooops, 沒有連接
打開連接
從剪貼板貼上
@@ -462,7 +488,7 @@
%1$s 網絡有一個 Existential Deposit 的概念。如果您的帳戶低於 %2$s,它將被停用,所有剩餘資金將被銷毀
此卡可能是生產樣本或偽造品
認證檢查失敗
- 重要安全信息%s
+ 重要安全信息 %s
此卡上只有 %s 個簽名可用。您必須提取所有資金
你喜歡 Tangem 嗎?
一個問題
diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml
index e0e0ef5d5b..eda6534993 100644
--- a/core/res/src/main/res/values/strings.xml
+++ b/core/res/src/main/res/values/strings.xml
@@ -27,6 +27,11 @@
Please scan the card
Please try again in 30 seconds or scan the card
Too many attempts
+ You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings. You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings.
+ Disable this option if you don\'t want this card to be used to reset access codes on other cards of this wallet. Note that you will not be able to reset access code on this card as well.
+ Allows you to use this card to reset access code on other cards in this wallet
+ Disable the ability to reset access code on this card or other cards in this wallet
+ Access code recovery
Reset
Are you sure you want to do this?
Change Access Code
@@ -36,6 +41,10 @@
Card Settings
Tangem Bot
Support
+ Rate operator
+ Send logs
+ Please select an action
+ Please, rate the work of the operator
Accept
Add
Attention
@@ -49,10 +58,15 @@
Copy
Create
Delete
+ Disabled
Disconnect
+ Dislike
Done
Enable
+ Enabled
Error
+ Import
+ Like
No
OK
Primary Card
@@ -173,11 +187,15 @@
Let\'s generate all the keys on your card and create a secure wallet
Create wallet
Create a wallet
+ Other options
+ Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it.
+ Generate keys privately
Your card is activated and ready to be used
Success!
In this case, you will need to start from the beginning.
Do you want to exit the activation process?
Getting started
+ Another wallet has already been created on the card you\'re trying to add. Do you want to reset it and use the card for a new wallet?
Verify your identity
KYC
Pin code
@@ -190,6 +208,20 @@
No backup card
Backup card ready
Prepare the SaltPay card
+ Read more about seed phrases
+ Write these 12 words down in the order given below and store them in a safe and secret place.
+ Your secret phrase
+ To import your wallet, enter your secret phrase in the field below
+ Generate seed phrase
+ Import wallet
+ A secret phrase is a series of words that allows you to recover your wallet. Unlike the keys generated by the card, secret phrases are unprotected and can be copied and stolen. Use this option at your own risk.
+ Use seed phrase
+ Invalid secret phrase. Please check words order.
+ Invalid secret phrase. Please check your spelling.
+ Legacy
+ We do not recommend storing the passphrase as a screenshot due to the high risk of loss or hacking
+ To check whether you’ve written down your secret phrase correctly, please enter the 2nd, 7th and 11th words
+ So, let’s check
To get started, simply claim wxDAI to your wallet
It will take a few seconds
Please check your email for further instructions
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/Notifications.kt b/core/ui/src/main/java/com/tangem/core/ui/components/Notifications.kt
new file mode 100644
index 0000000000..e70f571b18
--- /dev/null
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/Notifications.kt
@@ -0,0 +1,202 @@
+package com.tangem.core.ui.components
+
+import androidx.annotation.DrawableRes
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+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.res.painterResource
+import androidx.compose.ui.tooling.preview.Preview
+import com.tangem.core.ui.R
+import com.tangem.core.ui.res.TangemTheme
+
+/**
+ * Closable notification with custom icon
+ * Child of parent component
+ * @see Figma component
+ *
+ * Use to show banner with custom icon and possibility to close
+ * i.e. Feedback notification
+ *
+ * @param title notification title
+ * @param icon drawable res on icon
+ * @param iconColor icon color
+ * @param onClick callback on click
+ * @param onCloseClick callback on close icon click
+ */
+@Composable
+fun ClosableNotification(
+ title: String,
+ @DrawableRes icon: Int,
+ iconColor: Color,
+ onClick: (() -> Unit),
+ onCloseClick: (() -> Unit),
+) {
+ NotificationCardTemplate(onClick) {
+ Icon(
+ modifier = Modifier
+ .size(TangemTheme.dimens.size20)
+ .align(Alignment.CenterStart),
+ painter = painterResource(id = icon),
+ tint = iconColor,
+ contentDescription = null,
+ )
+ Text(
+ modifier = Modifier
+ .padding(horizontal = TangemTheme.dimens.spacing28)
+ .align(Alignment.CenterStart),
+ text = title,
+ color = TangemTheme.colors.text.primary1,
+ style = TangemTheme.typography.subtitle2,
+ )
+ Icon(
+ modifier = Modifier
+ .size(TangemTheme.dimens.size20)
+ .align(Alignment.CenterEnd)
+ .clickable(onClick = onCloseClick),
+ painter = painterResource(id = R.drawable.ic_close_24),
+ contentDescription = null,
+ tint = TangemTheme.colors.icon.informative,
+ )
+ }
+}
+
+/**
+ * Notification component from Design system
+ * There are few states for this component, but only one parent, see link below
+ *
+ * Use this for Notification with title, subtitle, clickable or not
+ *
+ * @param title notification title
+ * @param subtitle notification subtitle
+ * @param onClick click on notification, if its null then no chevron icon
+ *
+ * @see Figma component
+ */
+@Composable
+fun WarningNotification(title: String, subtitle: String?, onClick: (() -> Unit)?) {
+ NotificationCardTemplate(onClick) {
+ Image(
+ modifier = Modifier
+ .size(TangemTheme.dimens.size20)
+ .align(Alignment.CenterStart),
+ painter = painterResource(id = R.drawable.img_attention_20),
+ contentDescription = null,
+ )
+ Column(
+ modifier = Modifier
+ .padding(horizontal = TangemTheme.dimens.spacing28)
+ .align(Alignment.CenterStart),
+ ) {
+ Text(
+ text = title,
+ color = TangemTheme.colors.text.primary1,
+ style = TangemTheme.typography.subtitle2,
+ )
+ if (!subtitle.isNullOrEmpty()) {
+ SpacerH2()
+ Text(
+ text = subtitle,
+ color = TangemTheme.colors.text.tertiary,
+ style = TangemTheme.typography.caption,
+ )
+ }
+ }
+ if (onClick != null) {
+ Icon(
+ modifier = Modifier
+ .size(TangemTheme.dimens.size20)
+ .align(Alignment.CenterEnd),
+ painter = painterResource(id = R.drawable.ic_chevron_right_24),
+ contentDescription = null,
+ tint = TangemTheme.colors.icon.informative,
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+private fun NotificationCardTemplate(onClick: (() -> Unit)? = null, content: @Composable BoxScope.() -> Unit) {
+ Surface(
+ color = TangemTheme.colors.button.secondary,
+ shape = RoundedCornerShape(TangemTheme.dimens.radius18),
+ onClick = onClick ?: {},
+ enabled = onClick != null,
+ ) {
+ Box(
+ Modifier
+ .padding(
+ horizontal = TangemTheme.dimens.spacing12,
+ vertical = TangemTheme.dimens.spacing8,
+ )
+ .wrapContentSize(),
+ ) {
+ content()
+ }
+ }
+}
+
+// region Preview
+
+@Composable
+private fun WarningNotificationPreview() {
+ Column(modifier = Modifier.fillMaxWidth()) {
+ WarningNotification(
+ title = "Your wallet hasn’t been backed up",
+ subtitle = "Lorem ipsum dolor sit amet, consectetur " +
+ "adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
+ onClick = {},
+ )
+ SpacerH32()
+ WarningNotification(
+ title = "Your wallet hasn’t been backed up",
+ subtitle = null,
+ onClick = {},
+ )
+ SpacerH32()
+ WarningNotification(
+ title = "Your wallet hasn’t been backed up",
+ subtitle = "Lorem ipsum dolor sit amet, consectetur " +
+ "adipiscing elit, sed do eiusmod tempor incididunt ut labore et...",
+ onClick = null,
+ )
+ SpacerH32()
+ WarningNotification(
+ title = "Your wallet hasn’t been backed up",
+ subtitle = null,
+ onClick = null,
+ )
+ SpacerH32()
+ ClosableNotification(
+ title = "Like tangem app?",
+ icon = R.drawable.ic_star_24,
+ iconColor = TangemTheme.colors.icon.attention,
+ onClick = {},
+ onCloseClick = {},
+ )
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun Preview_WarningNotification_InLightTheme() {
+ TangemTheme(isDark = false) {
+ WarningNotificationPreview()
+ }
+}
+
+@Preview(showBackground = true)
+@Composable
+private fun Preview_WarningNotification_InDarkTheme() {
+ TangemTheme(isDark = true) {
+ WarningNotificationPreview()
+ }
+}
+
+// endregion Preview
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt
index eafe741803..2575fee4ad 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/components/TextFields.kt
@@ -141,19 +141,27 @@ private fun TangemTextField(
},
trailingIcon = {
val iconRes by rememberUpdatedState(
- newValue = if (isError) R.drawable.ic_alert_24 else R.drawable.ic_close_24,
+ newValue = if (isError) {
+ R.drawable.ic_alert_24
+ } else if (value.text.isNotEmpty()) {
+ R.drawable.ic_close_24
+ } else {
+ null
+ },
)
- IconButton(
- modifier = Modifier.size(32.dp),
- onClick = onClear,
- enabled = !isError && enabled,
- ) {
- Icon(
- modifier = Modifier.size(24.dp),
- painter = painterResource(id = iconRes),
- tint = colors.trailingIconColor(enabled = enabled, isError = isError).value,
- contentDescription = "Clear input",
- )
+ if (iconRes != null) {
+ IconButton(
+ modifier = Modifier.size(32.dp),
+ onClick = onClear,
+ enabled = !isError && enabled,
+ ) {
+ Icon(
+ modifier = Modifier.size(24.dp),
+ painter = painterResource(id = iconRes!!),
+ tint = colors.trailingIconColor(enabled = enabled, isError = isError).value,
+ contentDescription = "Clear input",
+ )
+ }
}
},
)
@@ -193,7 +201,7 @@ private fun TangemTextFieldSize.toShape(): Shape = when (this) {
TangemTextFieldSize.Default -> TangemTheme.shapes.roundedCornersSmall2
}
-internal object TangemTextFieldsDefault {
+object TangemTextFieldsDefault {
val defaultTextFieldColors: TangemTextFieldColors
@Composable @Stable get() = TangemTextFieldColors(
textColor = TangemTheme.colors.text.primary1,
@@ -224,7 +232,7 @@ internal object TangemTextFieldsDefault {
}
@Immutable
-internal data class TangemTextFieldColors(
+data class TangemTextFieldColors(
private val textColor: Color,
private val disabledTextColor: Color,
private val cursorColor: Color,
diff --git a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt
index 2c13836580..d760b3a96b 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/extensions/BlockchainIcons.kt
@@ -35,6 +35,7 @@ fun getActiveIconRes(blockchainId: String): Int {
"KAS" -> R.drawable.img_kaspa_22
"The-Open-Network", "The-Open-Network/test" -> R.drawable.img_ton_22
"KAVA", "KAVA/test" -> R.drawable.img_kava_22
+ "ravencoin", "ravencoin/test" -> R.drawable.img_ravencoin_22
else -> R.drawable.ic_alert_24
}
}
@@ -76,6 +77,7 @@ fun getActiveIconResByCoinId(coinId: String, networkId: String): Int {
"kaspa" -> R.drawable.img_kaspa_22
"ton" -> R.drawable.img_ton_22
"kava" -> R.drawable.img_kava_22
+ "ravencoin" -> R.drawable.img_ravencoin_22
else -> R.drawable.ic_alert_24
}
}
\ No newline at end of file
diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt
index 256d81f8e8..d5d14244ae 100644
--- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt
+++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemDimens.kt
@@ -28,6 +28,7 @@ data class TangemDimens internal constructor(
val radius10: Dp = 10.dp,
val radius12: Dp = 12.dp,
val radius16: Dp = 16.dp,
+ val radius18: Dp = 18.dp,
val radius28: Dp = 28.dp,
// endregion Radius
// region Size
@@ -47,6 +48,7 @@ data class TangemDimens internal constructor(
val size24: Dp = 24.dp,
val size28: Dp = 28.dp,
val size32: Dp = 32.dp,
+ val size34: Dp = 34.dp,
val size36: Dp = 36.dp,
val size40: Dp = 40.dp,
val size42: Dp = 42.dp,
@@ -85,6 +87,7 @@ data class TangemDimens internal constructor(
val spacing28: Dp = 28.dp,
val spacing32: Dp = 32.dp,
val spacing34: Dp = 34.dp,
+ val spacing36: Dp = 34.dp,
val spacing38: Dp = 38.dp,
val spacing44: Dp = 44.dp,
val spacing50: Dp = 50.dp,
diff --git a/core/ui/src/main/res/drawable/card_placeholder_black.xml b/core/ui/src/main/res/drawable/card_placeholder_black.xml
new file mode 100644
index 0000000000..83fab8e0f7
--- /dev/null
+++ b/core/ui/src/main/res/drawable/card_placeholder_black.xml
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_onboarding_text_edit_56.xml b/core/ui/src/main/res/drawable/ic_onboarding_text_edit_56.xml
new file mode 100644
index 0000000000..11e420c412
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_onboarding_text_edit_56.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/ic_star_24.xml b/core/ui/src/main/res/drawable/ic_star_24.xml
new file mode 100644
index 0000000000..caa0be3b55
--- /dev/null
+++ b/core/ui/src/main/res/drawable/ic_star_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/core/ui/src/main/res/drawable/img_ravencoin_22.xml b/core/ui/src/main/res/drawable/img_ravencoin_22.xml
new file mode 100644
index 0000000000..49c47eb9da
--- /dev/null
+++ b/core/ui/src/main/res/drawable/img_ravencoin_22.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt
index 952424e215..6f28c0350e 100644
--- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt
+++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineDispatcherProvider.kt
@@ -2,19 +2,25 @@ package com.tangem.utils.coroutines
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.asCoroutineDispatcher
+import java.util.concurrent.Executors
import javax.inject.Inject
interface CoroutineDispatcherProvider {
val main: CoroutineDispatcher
val io: CoroutineDispatcher
+ val single: CoroutineDispatcher
}
class AppCoroutineDispatcherProvider @Inject constructor() : CoroutineDispatcherProvider {
override val main: CoroutineDispatcher = Dispatchers.Main
override val io: CoroutineDispatcher = Dispatchers.IO
+ override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher()
}
class TestingCoroutineDispatcherProvider(
override val main: CoroutineDispatcher = Dispatchers.Unconfined,
override val io: CoroutineDispatcher = Dispatchers.Unconfined,
-) : CoroutineDispatcherProvider
\ No newline at end of file
+ override val single: CoroutineDispatcher = Executors.newFixedThreadPool(1).asCoroutineDispatcher(),
+) : CoroutineDispatcherProvider
+
diff --git a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt
index 59452dbf7a..872aeb1c64 100644
--- a/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt
+++ b/core/utils/src/main/java/com/tangem/utils/coroutines/CoroutineExt.kt
@@ -8,6 +8,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
suspend fun runCatching(dispatcher: CoroutineDispatcher, block: suspend () -> R): Result {
return runCatching {
@@ -25,11 +27,20 @@ class Debouncer {
private var debounceJob: Job? = null
- fun debounce(waitMs: Long = 300L, coroutineScope: CoroutineScope, destinationFunction: () -> Unit) {
+ fun debounce(
+ coroutineScope: CoroutineScope,
+ waitMs: Long = 300L,
+ context: CoroutineContext = EmptyCoroutineContext,
+ destinationFunction: suspend () -> Unit,
+ ) {
debounceJob?.cancel()
- debounceJob = coroutineScope.launch {
+ debounceJob = coroutineScope.launch(context) {
delay(waitMs)
destinationFunction.invoke()
}
}
+
+ fun release() {
+ debounceJob?.cancel()
+ }
}
\ No newline at end of file
diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Char.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Char.kt
new file mode 100644
index 0000000000..0eed425110
--- /dev/null
+++ b/core/utils/src/main/java/com/tangem/utils/extensions/Char.kt
@@ -0,0 +1,6 @@
+package com.tangem.utils.extensions
+
+/**
+[REDACTED_AUTHOR]
+ */
+fun Char.isNotWhitespace(): Boolean = !this.isWhitespace()
\ No newline at end of file
diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt
new file mode 100644
index 0000000000..89fd92851b
--- /dev/null
+++ b/core/utils/src/main/java/com/tangem/utils/extensions/Collection.kt
@@ -0,0 +1,6 @@
+package com.tangem.utils.extensions
+
+/**
+[REDACTED_AUTHOR]
+ */
+fun Collection.isSingleItem(): Boolean = this.size == 1
\ No newline at end of file
diff --git a/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt b/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt
new file mode 100644
index 0000000000..af00c9f75a
--- /dev/null
+++ b/core/utils/src/main/java/com/tangem/utils/extensions/Int.kt
@@ -0,0 +1,8 @@
+package com.tangem.utils.extensions
+
+/**
+[REDACTED_AUTHOR]
+ */
+fun Int.isEven(): Boolean = this % 2 == 0
+
+fun Int.isOdd(): Boolean = !this.isEven()
\ No newline at end of file
diff --git a/domain/card/.gitignore b/domain/card/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/domain/card/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/domain/card/build.gradle.kts b/domain/card/build.gradle.kts
new file mode 100644
index 0000000000..0da92b17b4
--- /dev/null
+++ b/domain/card/build.gradle.kts
@@ -0,0 +1,18 @@
+plugins {
+ alias(deps.plugins.kotlin.jvm)
+ alias(deps.plugins.kotlin.kapt)
+ id("configuration")
+}
+
+dependencies {
+ implementation(project(":domain:core"))
+ // TODO: Remove after new card scan result was implemented
+ implementation(project(":domain:models"))
+
+ implementation(deps.tangem.card.core)
+
+ implementation(deps.hilt.core)
+ kapt(deps.hilt.kapt)
+
+ implementation(deps.moshi.kotlin)
+}
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt
new file mode 100644
index 0000000000..b1000aa283
--- /dev/null
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardException.kt
@@ -0,0 +1,14 @@
+package com.tangem.domain.card
+
+import com.tangem.common.core.TangemSdkError
+
+// TODO: May be add new error types
+sealed class ScanCardException : Exception() {
+ object WrongCardId : ScanCardException()
+
+ object UserCancelled : ScanCardException()
+
+ open class ChainException : ScanCardException()
+
+ class SdkException(override val cause: TangemSdkError) : ScanCardException()
+}
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt
new file mode 100644
index 0000000000..88de647d8d
--- /dev/null
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/ScanCardUseCase.kt
@@ -0,0 +1,76 @@
+package com.tangem.domain.card
+
+import arrow.core.EitherNel
+import arrow.core.flatMap
+import arrow.core.toEitherNel
+import com.tangem.TangemSdk
+import com.tangem.common.core.CardIdDisplayFormat
+import com.tangem.domain.card.model.ScanCardParams
+import com.tangem.domain.card.repository.ScanCardRepository
+import com.tangem.domain.core.chain.ChainProcessor
+import com.tangem.domain.models.scan.ProductType
+import com.tangem.domain.models.scan.ScanResponse
+import javax.inject.Inject
+
+/**
+ * Use case responsible for scanning a card and returning a [ScanResponse] object.
+ * @property scanCardRepository A repository object implementing [ScanCardRepository] interface.
+ * @property tangemSdk An instance of [TangemSdk] to configure the display format of the card ID.
+ * @constructor Create a new instance of [ScanCardUseCase] with the given dependencies.
+ */
+class ScanCardUseCase @Inject internal constructor(
+ private val scanCardRepository: ScanCardRepository,
+ private val tangemSdk: TangemSdk,
+) {
+
+ /**
+ * A [ChainProcessor] object to launch the after-scan chains.
+ */
+ private val scanChainProcessor by lazy {
+ ChainProcessor()
+ }
+
+ /**
+ * Scan a card and return a [ScanResponse] object.
+ * @param params An instance of [ScanCardParams] to configure the scan process.
+ * @return A [EitherNel] object with either a non-empty list of [ScanCardException] or a [ScanResponse].
+ */
+ suspend operator fun invoke(params: ScanCardParams = ScanCardParams()): EitherNel {
+ resetCardIdDisplayFormat()
+ scanChainProcessor.addChains(*params.afterScanChains)
+
+ return scanCardRepository.scanCard(
+ cardId = params.cardId,
+ allowRequestAccessCodeFromRepository = params.allowRequestAccessCodeFromRepository,
+ )
+ .onRight { scanResponse ->
+ updateCardIdDisplayFormat(scanResponse.productType)
+ }
+ .toEitherNel()
+ .flatMap { scanResponse ->
+ scanChainProcessor.launchChains(initial = scanResponse)
+ }
+ }
+
+ /**
+ * Reset the card ID display format to [CardIdDisplayFormat.Full].
+ */
+ private fun resetCardIdDisplayFormat() {
+ tangemSdk.config.cardIdDisplayFormat = CardIdDisplayFormat.Full
+ }
+
+ /**
+ * Update the card ID display format according to the [ProductType] of the scanned card.
+ * @param productType The [ProductType] of the scanned card.
+ */
+ private fun updateCardIdDisplayFormat(productType: ProductType) {
+ tangemSdk.config.cardIdDisplayFormat = when (productType) {
+ ProductType.Twins -> CardIdDisplayFormat.LastLuhn(numbers = 4)
+ ProductType.SaltPay -> CardIdDisplayFormat.None
+ ProductType.Note,
+ ProductType.Wallet,
+ ProductType.Start2Coin,
+ -> CardIdDisplayFormat.Full
+ }
+ }
+}
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/di/CardDomainModule.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/di/CardDomainModule.kt
new file mode 100644
index 0000000000..592df6a876
--- /dev/null
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/di/CardDomainModule.kt
@@ -0,0 +1,18 @@
+package com.tangem.domain.card.di
+
+import com.tangem.TangemSdk
+import com.tangem.domain.card.ScanCardUseCase
+import com.tangem.domain.card.repository.ScanCardRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+class CardDomainModule {
+
+ @Provides
+ fun provideScanCardUseCase(scanCardRepository: ScanCardRepository, tangemSdk: TangemSdk) =
+ ScanCardUseCase(scanCardRepository, tangemSdk)
+}
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/model/ScanCardParams.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/model/ScanCardParams.kt
new file mode 100644
index 0000000000..2fa7fd798d
--- /dev/null
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/model/ScanCardParams.kt
@@ -0,0 +1,21 @@
+package com.tangem.domain.card.model
+
+import com.tangem.domain.card.ScanCardException
+import com.tangem.domain.core.chain.Chain
+import com.tangem.domain.models.scan.ScanResponse
+
+/**
+ * Represents the parameters to be used during a card scanning operation.
+ *
+ * @property cardId an optional card ID to scan. If null, can scan any card present.
+ * Defaults to null.
+ * @property allowRequestAccessCodeFromRepository whether to prompt the user for an access code if needed.
+ * Defaults to false.
+ * @property afterScanChains An array of chains that should be executed after a successful card scan operation.
+ * Defaults to an empty array.
+ */
+class ScanCardParams(
+ val cardId: String? = null,
+ val allowRequestAccessCodeFromRepository: Boolean = false,
+ val afterScanChains: Array> = emptyArray(),
+)
\ No newline at end of file
diff --git a/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt
new file mode 100644
index 0000000000..0fce5103c1
--- /dev/null
+++ b/domain/card/src/main/kotlin/com/tangem/domain/card/repository/ScanCardRepository.kt
@@ -0,0 +1,24 @@
+package com.tangem.domain.card.repository
+
+import arrow.core.Either
+import com.tangem.domain.card.ScanCardException
+import com.tangem.domain.models.scan.ScanResponse
+
+/**
+ * A repository for scanning a card.
+ */
+interface ScanCardRepository {
+
+ /**
+ * Scans the card with the given [cardId] and returns a [ScanResponse]. If [allowRequestAccessCodeFromRepository]
+ * is true, the repository may prompt the user for an access code.
+ *
+ * @param cardId an optional card ID to scan. If null, the repository should scan any card present.
+ * @param allowRequestAccessCodeFromRepository whether to prompt the user for an access code if needed.
+ * @return an [Either] that contains a [ScanCardException] in case of an error or a [ScanResponse].
+ */
+ suspend fun scanCard(
+ cardId: String?,
+ allowRequestAccessCodeFromRepository: Boolean,
+ ): Either
+}
\ No newline at end of file
diff --git a/domain/core/.gitignore b/domain/core/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/domain/core/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/domain/core/build.gradle.kts b/domain/core/build.gradle.kts
new file mode 100644
index 0000000000..63325166f7
--- /dev/null
+++ b/domain/core/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ alias(deps.plugins.kotlin.jvm)
+ id("configuration")
+}
+
+dependencies {
+ api(deps.kotlin.coroutines)
+ api(deps.arrow.core)
+ api(deps.arrow.fx)
+}
\ No newline at end of file
diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt
new file mode 100644
index 0000000000..57ec9f59b2
--- /dev/null
+++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/Chain.kt
@@ -0,0 +1,19 @@
+package com.tangem.domain.core.chain
+
+import arrow.core.Either
+import arrow.core.EitherNel
+
+/**
+ * A chain in the [ChainProcessor] class for processing a chain of operations with the ability to handle errors.
+ * @param E the type of error
+ * @param R the type of result
+ */
+interface Chain {
+
+ /**
+ * Invokes the chain with the previous chain result as input and returns an [Either] result.
+ * @param previousChainResult the previous chain result as an [EitherNel]
+ * @return the result of the chain processing as an [Either]
+ */
+ suspend operator fun invoke(previousChainResult: EitherNel): Either
+}
\ No newline at end of file
diff --git a/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt
new file mode 100644
index 0000000000..334af39b0e
--- /dev/null
+++ b/domain/core/src/main/kotlin/com/tangem/domain/core/chain/ChainProcessor.kt
@@ -0,0 +1,63 @@
+package com.tangem.domain.core.chain
+
+import arrow.core.Either
+import arrow.core.EitherNel
+import arrow.core.left
+import arrow.core.nel
+import arrow.core.right
+import arrow.core.toEitherNel
+
+/**
+ * A class for processing a chain of operations with the ability to handle errors.
+ * @param E the type of error
+ * @param R the type of result
+ */
+class ChainProcessor {
+
+ /**
+ * The list of chains to be executed in order.
+ */
+ private val chains: MutableList> = mutableListOf()
+
+ /**
+ * Adds chains to the existing list of chains to be executed.
+ * @param chains the chains to be added to the list
+ */
+ fun addChains(vararg chains: Chain) {
+ this.chains.addAll(chains)
+ }
+
+ /**
+ * Launches the chains with the given initial value as input.
+ * @param initial the initial value to start the chain processing.
+ * @param accumulateExceptions determines whether to accumulate exceptions or break on first exception.
+ * Defaults to true.
+ * @return the result of the chain processing as an EitherNel, which is a disjunction that may contain multiple errors.
+ */
+ private suspend fun launchChains(initial: Either, accumulateExceptions: Boolean = true): EitherNel {
+ return chains.fold(initial.toEitherNel()) { previousChainResult, chain ->
+ chain.invoke(previousChainResult)
+ .mapLeft { e ->
+ previousChainResult.leftOrNull()?.plus(e)
+ ?: e.nel()
+ }
+ .onLeft { e ->
+ if (!accumulateExceptions) {
+ return e.left()
+ }
+ }
+ }
+ }
+
+ /**
+ * Launches the chains with the given initial value as input.
+ * This function is a convenience function that assumes the initial value does not contain any errors.
+ * @param initial the initial value to start the chain processing.
+ * @param accumulateExceptions determines whether to accumulate exceptions or break on first exception.
+ * Defaults to true.
+ * @return the result of the chain processing as an EitherNel, which is a disjunction that may contain multiple errors.
+ */
+ suspend fun launchChains(initial: R, accumulateExceptions: Boolean = true): EitherNel {
+ return launchChains(initial.right(), accumulateExceptions)
+ }
+}
\ No newline at end of file
diff --git a/domain/.gitignore b/domain/legacy/.gitignore
similarity index 100%
rename from domain/.gitignore
rename to domain/legacy/.gitignore
diff --git a/domain/build.gradle.kts b/domain/legacy/build.gradle.kts
similarity index 96%
rename from domain/build.gradle.kts
rename to domain/legacy/build.gradle.kts
index 6ea5884226..f618e11fd9 100644
--- a/domain/build.gradle.kts
+++ b/domain/legacy/build.gradle.kts
@@ -9,6 +9,7 @@ dependencies {
implementation(project(":core:utils"))
implementation(project(":common"))
implementation(project(":libs:auth"))
+ implementation(project(":domain:models"))
/** Tangem libraries */
implementation(deps.tangem.blockchain) {
diff --git a/domain/proguard-rules.pro b/domain/legacy/proguard-rules.pro
similarity index 100%
rename from domain/proguard-rules.pro
rename to domain/legacy/proguard-rules.pro
diff --git a/domain/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt b/domain/legacy/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt
similarity index 100%
rename from domain/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt
rename to domain/legacy/src/androidTest/java/com/tangem/domain/features/ExampleInstrumentedTest.kt
diff --git a/domain/src/main/AndroidManifest.xml b/domain/legacy/src/main/AndroidManifest.xml
similarity index 100%
rename from domain/src/main/AndroidManifest.xml
rename to domain/legacy/src/main/AndroidManifest.xml
diff --git a/domain/src/main/java/com/tangem/domain/DomainDialog.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainDialog.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/DomainDialog.kt
rename to domain/legacy/src/main/java/com/tangem/domain/DomainDialog.kt
diff --git a/domain/src/main/java/com/tangem/domain/DomainLayer.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/DomainLayer.kt
rename to domain/legacy/src/main/java/com/tangem/domain/DomainLayer.kt
diff --git a/domain/src/main/java/com/tangem/domain/DomainModuleMessage.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/DomainModuleMessage.kt
rename to domain/legacy/src/main/java/com/tangem/domain/DomainModuleMessage.kt
diff --git a/domain/src/main/java/com/tangem/domain/DomainWrapped.kt b/domain/legacy/src/main/java/com/tangem/domain/DomainWrapped.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/DomainWrapped.kt
rename to domain/legacy/src/main/java/com/tangem/domain/DomainWrapped.kt
diff --git a/domain/src/main/java/com/tangem/domain/common/CardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt
similarity index 94%
rename from domain/src/main/java/com/tangem/domain/common/CardTypesResolver.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt
index a938ae53b0..0d91e233df 100644
--- a/domain/src/main/java/com/tangem/domain/common/CardTypesResolver.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/CardTypesResolver.kt
@@ -6,6 +6,7 @@ import com.tangem.blockchain.common.Token
interface CardTypesResolver {
fun isTangemNote(): Boolean
fun isTangemWallet(): Boolean
+ fun isWallet2(): Boolean
fun isSaltPay(): Boolean
fun isSaltPayVisa(): Boolean
fun isSaltPayWallet(): Boolean
diff --git a/domain/src/main/java/com/tangem/domain/common/LogConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/LogConfig.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/LogConfig.kt
diff --git a/domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt b/domain/legacy/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/SaltPayWorkaround.kt
diff --git a/domain/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt
similarity index 94%
rename from domain/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt
index 5d9f6fafa7..d62e391dfb 100644
--- a/domain/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/TangemCardTypesResolver.kt
@@ -11,6 +11,8 @@ import com.tangem.domain.common.TapWorkarounds.isSaltPayVisa
import com.tangem.domain.common.TapWorkarounds.isSaltPayWallet
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.domain.common.TapWorkarounds.isTestCard
+import com.tangem.domain.models.scan.CardDTO
+import com.tangem.domain.models.scan.ProductType
class TangemCardTypesResolver(
private val card: CardDTO,
@@ -24,6 +26,7 @@ class TangemCardTypesResolver(
card.firmwareVersion >= FirmwareVersion.MultiWalletAvailable &&
!card.isSaltPay
+ override fun isWallet2(): Boolean = card.firmwareVersion >= FirmwareVersion.KeysImportAvailable
override fun isSaltPay(): Boolean = productType == ProductType.SaltPay
override fun isSaltPayVisa(): Boolean = card.isSaltPayVisa
override fun isSaltPayWallet(): Boolean = card.isSaltPayWallet
@@ -79,8 +82,4 @@ private fun Blockchain.Companion.fromBlockchainName(blockchainName: String): Blo
Blockchain.fromId(blockchainName)
}
}
-}
-
-enum class ProductType {
- Note, Twins, Wallet, SaltPay, Start2Coin
}
\ No newline at end of file
diff --git a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt
similarity index 95%
rename from domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt
index 9ac3308197..c03bb09cda 100644
--- a/domain/src/main/java/com/tangem/domain/common/TapWorkarounds.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/TapWorkarounds.kt
@@ -3,6 +3,7 @@ package com.tangem.domain.common
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.DerivationStyle
import com.tangem.common.card.Card
+import com.tangem.domain.models.scan.CardDTO
import java.util.*
/**
@@ -16,10 +17,6 @@ object TapWorkarounds {
val CardDTO.isTangemTwins: Boolean
get() = TwinsHelper.getTwinCardNumber(cardId) != null
- // TODO: replace by reading files from a card
- val CardDTO.isTangemNote: Boolean
- get() = tangemNoteBatches.contains(batchId)
-
val CardDTO.isStart2Coin: Boolean
get() = isStart2CoinIssuer(issuer.name)
diff --git a/domain/src/main/java/com/tangem/domain/common/Throttling.kt b/domain/legacy/src/main/java/com/tangem/domain/common/Throttling.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/Throttling.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/Throttling.kt
diff --git a/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt
similarity index 97%
rename from domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt
index 7e801695fb..f6aa02a2c5 100644
--- a/domain/src/main/java/com/tangem/domain/common/TwinsHelper.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt
@@ -1,6 +1,7 @@
package com.tangem.domain.common
import com.tangem.crypto.CryptoUtils
+import com.tangem.domain.models.scan.CardDTO
object TwinsHelper {
private val firstCardSeries = listOf("CB61", "CB64")
diff --git a/domain/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt b/domain/legacy/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/demo/DemoConfig.kt
diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt
similarity index 96%
rename from domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt
index 40bb83cc64..fe9b1dfc51 100644
--- a/domain/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Blockchain.kt
@@ -54,6 +54,8 @@ fun Blockchain.Companion.fromNetworkId(networkId: String): Blockchain? {
"the-open-network/test" -> Blockchain.TONTestnet
"kava" -> Blockchain.Kava
"kava/test" -> Blockchain.KavaTestnet
+ "ravencoin" -> Blockchain.Ravencoin
+ "ravencoin/test" -> Blockchain.RavencoinTestnet
else -> null
}
}
@@ -112,6 +114,8 @@ fun Blockchain.toNetworkId(): String {
Blockchain.TONTestnet -> "the-open-network/test"
Blockchain.Kava -> "kava"
Blockchain.KavaTestnet -> "kava/test"
+ Blockchain.Ravencoin -> "ravencoin"
+ Blockchain.RavencoinTestnet -> "ravencoin/test"
}
}
@@ -149,6 +153,7 @@ fun Blockchain.toCoinId(): String {
Blockchain.TON, Blockchain.TONTestnet -> "the-open-network"
Blockchain.Unknown -> "unknown"
Blockchain.Kava, Blockchain.KavaTestnet -> "kava"
+ Blockchain.Ravencoin, Blockchain.RavencoinTestnet -> "ravencoin"
}
}
diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/ByteArray.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/ByteArray.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/extensions/ByteArray.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/extensions/ByteArray.kt
diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt
similarity index 96%
rename from domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt
index c7d205bbe0..77e154859f 100644
--- a/domain/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/CardSdk.kt
@@ -3,8 +3,8 @@ package com.tangem.domain.common.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.card.EllipticCurve
import com.tangem.common.card.FirmwareVersion
-import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isTestCard
+import com.tangem.domain.models.scan.CardDTO
/**
[REDACTED_AUTHOR]
diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/extensions/Coroutine.kt
diff --git a/domain/src/main/java/com/tangem/domain/common/extensions/ResultCardSdk.kt b/domain/legacy/src/main/java/com/tangem/domain/common/extensions/ResultCardSdk.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/extensions/ResultCardSdk.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/extensions/ResultCardSdk.kt
diff --git a/domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt
similarity index 91%
rename from domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt
index f4800c7223..b406d02f7c 100644
--- a/domain/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldDataConverters.kt
@@ -13,7 +13,7 @@ interface DataConverterVisitor {
interface FieldDataConverter : DataConverterVisitor
abstract class BaseFieldDataConverter : FieldDataConverter {
- protected val collectIds: List
+ private val collectIds: List
get() = getIdToCollect()
protected val collectedData: MutableMap = mutableMapOf()
@@ -31,7 +31,7 @@ abstract class BaseFieldDataConverter : FieldDataConverter {
class FieldToJsonConverter(
private val fieldsToConvert: List = listOf(),
- protected val jsonConverter: MoshiJsonConverter,
+ private val jsonConverter: MoshiJsonConverter,
) : BaseFieldDataConverter() {
override fun getConvertedData(): String = jsonConverter.toJson(collectedData, " ")
diff --git a/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt
similarity index 78%
rename from domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt
index 8f5e7dfd00..067feb900f 100644
--- a/domain/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/form/FieldsValidators.kt
@@ -16,16 +16,14 @@ import timber.log.Timber
interface CustomTokenValidator : Validator
class StringIsEmptyValidator : CustomTokenValidator {
- override fun validate(data: String?): AddCustomTokenError? = when {
- data == null || data.isEmpty() -> null
- else -> AddCustomTokenError.FieldIsNotEmpty
+ override fun validate(data: String?): AddCustomTokenError? {
+ return if (data.isNullOrEmpty()) null else AddCustomTokenError.FieldIsNotEmpty
}
}
class StringIsNotEmptyValidator : CustomTokenValidator {
- override fun validate(data: String?): AddCustomTokenError? = when {
- data == null || data.isEmpty() -> AddCustomTokenError.FieldIsEmpty
- else -> null
+ override fun validate(data: String?): AddCustomTokenError? {
+ return if (data.isNullOrEmpty()) AddCustomTokenError.FieldIsEmpty else null
}
}
@@ -46,12 +44,10 @@ class TokenContractAddressValidator : CustomTokenValidator {
}
override fun validate(data: String?): AddCustomTokenError? {
- if (data == null || data.isEmpty()) return AddCustomTokenError.FieldIsEmpty
-
- return if (getAddressService().validate(data)) {
- null
- } else {
- AddCustomTokenError.InvalidContractAddress
+ return when {
+ data.isNullOrEmpty() -> AddCustomTokenError.FieldIsEmpty
+ getAddressService().validate(data) -> null
+ else -> AddCustomTokenError.InvalidContractAddress
}
}
@@ -74,9 +70,11 @@ class TokenContractAddressValidator : CustomTokenValidator {
}
class TokenNetworkValidator : CustomTokenValidator {
- override fun validate(data: Blockchain?): AddCustomTokenError? = when (data) {
- null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected
- else -> null
+ override fun validate(data: Blockchain?): AddCustomTokenError? {
+ return when (data) {
+ null, Blockchain.Unknown -> AddCustomTokenError.NetworkIsNotSelected
+ else -> null
+ }
}
}
@@ -88,14 +86,14 @@ class TokenSymbolValidator : CustomTokenValidator {
override fun validate(data: String?): AddCustomTokenError? = StringIsNotEmptyValidator().validate(data)
}
-@Suppress("MagicNumber")
class TokenDecimalsValidator : CustomTokenValidator {
override fun validate(data: String?): AddCustomTokenError? {
val decimal = data?.toIntOrNull() ?: return AddCustomTokenError.FieldIsEmpty
- return when {
- decimal > 30 -> AddCustomTokenError.InvalidDecimalsCount
- else -> null
- }
+ return if (decimal > INVALID_DECIMALS_COUNT) AddCustomTokenError.InvalidDecimalsCount else null
+ }
+
+ private companion object {
+ const val INVALID_DECIMALS_COUNT = 30
}
}
\ No newline at end of file
diff --git a/domain/src/main/java/com/tangem/domain/common/form/Form.kt b/domain/legacy/src/main/java/com/tangem/domain/common/form/Form.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/form/Form.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/form/Form.kt
diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt
new file mode 100644
index 0000000000..8438a87f14
--- /dev/null
+++ b/domain/legacy/src/main/java/com/tangem/domain/common/util/ScanResponseExtensions.kt
@@ -0,0 +1,47 @@
+package com.tangem.domain.common.util
+
+import com.tangem.blockchain.common.Blockchain
+import com.tangem.common.card.EllipticCurve
+import com.tangem.common.extensions.toMapKey
+import com.tangem.crypto.hdWallet.DerivationPath
+import com.tangem.domain.common.CardTypesResolver
+import com.tangem.domain.common.TangemCardTypesResolver
+import com.tangem.domain.common.TapWorkarounds.isTangemTwins
+import com.tangem.domain.common.TapWorkarounds.isTestCard
+import com.tangem.domain.models.scan.ScanResponse
+
+val ScanResponse.cardTypesResolver: CardTypesResolver
+ get() = TangemCardTypesResolver(
+ card = card,
+ productType = productType,
+ walletData = walletData,
+ )
+
+fun ScanResponse.twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null
+fun ScanResponse.supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed
+fun ScanResponse.supportsBackup(): Boolean = card.settings.isBackupAllowed
+
+fun ScanResponse.hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean {
+ return hasDerivation(blockchain, DerivationPath(rawDerivationPath))
+}
+
+private fun ScanResponse.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
+ }
+}
+
+private fun ScanResponse.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
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletId.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/util/UserWalletId.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/util/UserWalletId.kt
diff --git a/domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt b/domain/legacy/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt
rename to domain/legacy/src/main/java/com/tangem/domain/common/util/ValueDebouncer.kt
diff --git a/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt
new file mode 100644
index 0000000000..1a5fc65e8c
--- /dev/null
+++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt
@@ -0,0 +1,52 @@
+package com.tangem.domain.features.addCustomToken
+
+import com.tangem.datasource.api.tangemTech.TangemTechApi
+import com.tangem.datasource.api.tangemTech.models.CoinsResponse
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import kotlinx.coroutines.withContext
+
+/**
+[REDACTED_AUTHOR]
+ */
+class AddCustomTokenService(
+ private val tangemTechApi: TangemTechApi,
+ private val dispatchers: CoroutineDispatcherProvider,
+ private val supportedTokenNetworkIds: List,
+) {
+
+ suspend fun findToken(contractAddress: String, networkId: String?): List {
+ return withContext(dispatchers.io) {
+ runCatching {
+ tangemTechApi.getCoins(
+ contractAddress = contractAddress,
+ networkIds = selectNetworksForSearch(networkId),
+ )
+ }
+ .fold(
+ onSuccess = { response ->
+ var coinsList = mutableListOf()
+ response.coins.forEach { coin ->
+ val networksWithTheSameAddress = coin.networks
+ .filter { it.contractAddress != null || it.decimalCount != null }
+ .filter { it.contractAddress?.equals(contractAddress, ignoreCase = true) == true }
+ .filter { supportedTokenNetworkIds.contains(it.networkId) }
+ if (networksWithTheSameAddress.isNotEmpty()) {
+ val newToken = coin.copy(networks = networksWithTheSameAddress)
+ coinsList.add(newToken)
+ }
+ }
+ if (coinsList.size > 1) {
+ // https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679
+ coinsList = mutableListOf(coinsList[0])
+ }
+ coinsList
+ },
+ onFailure = { emptyList() },
+ )
+ }
+ }
+
+ private fun selectNetworksForSearch(networkId: String?): String {
+ return networkId ?: supportedTokenNetworkIds.joinToString(",")
+ }
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt
rename to domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/CustomCurrency.kt
diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt
rename to domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/FormFields.kt
diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt
similarity index 92%
rename from domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt
rename to domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt
index c2ae9ff018..c178c4a610 100644
--- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenAction.kt
@@ -1,7 +1,6 @@
package com.tangem.domain.features.addCustomToken.redux
import com.tangem.blockchain.common.Blockchain
-import com.tangem.blockchain.common.DerivationStyle
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.DomainWrapped
@@ -20,9 +19,7 @@ sealed class AddCustomTokenAction : Action {
data class SetOnAddTokenCallback(val callback: (CustomCurrency) -> Unit) : AddCustomTokenAction()
}
- object OnCreate : AddCustomTokenAction() {
- data class SetDerivationStyle(val derivationStyle: DerivationStyle?) : AddCustomTokenAction()
- }
+ object OnCreate : AddCustomTokenAction()
object OnDestroy : AddCustomTokenAction()
diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt
similarity index 96%
rename from domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt
rename to domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt
index 08315dc850..17f110b952 100644
--- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenHub.kt
@@ -125,7 +125,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT
}
}
}
- else -> {}
+ else -> Unit
}
}
@@ -342,7 +342,6 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT
}
}
- @Suppress("MagicNumber")
private suspend fun requestInfoAboutToken(contractAddress: String): List {
val tangemTechServiceManager = requireNotNull(hubState.tangemTechServiceManager)
dispatchOnMain(Screen.UpdateTokenFields(listOf(ContractAddress to ViewStates.TokenField(isLoading = true))))
@@ -354,7 +353,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT
// simulate loading effect. It would be better if the delay would only run if tokenManager.checkAddress()
// got the result faster than 500ms and the delay would only be the difference between them.
- delay(500)
+ delay(timeMillis = 500)
val result = tangemTechServiceManager.findToken(contractAddress, selectedNetworkId)
@@ -450,7 +449,7 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT
enableDisableTokenDetailFields(false)
}
- private suspend fun enableDisableTokenDetailFields(isEnabled: Boolean = true) {
+ private suspend fun enableDisableTokenDetailFields(isEnabled: Boolean) {
val state = hubState
val action = Screen.UpdateTokenFields(
listOf(
@@ -532,21 +531,24 @@ internal class AddCustomTokenHub : BaseStoreHub("AddCustomT
}
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 -> {
+ val contractAddressValidator: TokenContractAddressValidator = hubState.getValidator(ContractAddress)
contractAddressValidator.nextValidationFor(Network.getFieldValue())
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)
+ Network, DerivationPath -> {
+ hubState.getValidator(Network).validate(value as Blockchain)
+ }
+ Name -> {
+ hubState.getValidator(Name).validate(value as String)
+ }
+ Symbol -> {
+ hubState.getValidator(Symbol).validate(value as String)
+ }
+ Decimals -> {
+ hubState.getValidator(Decimals).validate(value as String)
+ }
}
}
@@ -587,17 +589,18 @@ private class AddCustomTokenReducer(
is OnCreate -> {
val card = requireNotNull(globalState.scanResponse?.card)
val supportedTokenNetworkIds = card.supportedBlockchains()
- .filter { it.canHandleTokens() }
- .map { it.toNetworkId() }
+ .filter(Blockchain::canHandleTokens)
+ .map(Blockchain::toNetworkId)
+
val tangemTechServiceManager = AddCustomTokenService(
tangemTechApi = globalState.networkServices.tangemTechService.api,
dispatchers = AppCoroutineDispatcherProvider(),
supportedTokenNetworkIds = supportedTokenNetworkIds,
)
- val form = Form(AddCustomTokenState.createFormFields(card, CustomTokenType.Blockchain))
+
state.copy(
cardDerivationStyle = card.derivationStyle,
- form = form,
+ form = Form(AddCustomTokenState.createFormFields(card, CustomTokenType.Blockchain)),
tangemTechServiceManager = tangemTechServiceManager,
screenState = createInitialScreenState(card.settings.isHDWalletAllowed),
)
diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt
similarity index 96%
rename from domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt
rename to domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt
index 7076b19233..3377646d25 100644
--- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/AddCustomTokenState.kt
@@ -6,7 +6,6 @@ import com.tangem.common.json.MoshiJsonConverter
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.AddCustomTokenError
import com.tangem.domain.DomainWrapped
-import com.tangem.domain.common.CardDTO
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.common.extensions.isSupportedInApp
import com.tangem.domain.common.extensions.supportedBlockchains
@@ -36,6 +35,7 @@ import com.tangem.domain.features.addCustomToken.CustomTokenFieldId.Symbol
import com.tangem.domain.features.addCustomToken.TokenBlockchainField
import com.tangem.domain.features.addCustomToken.TokenDerivationPathField
import com.tangem.domain.features.addCustomToken.TokenField
+import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.redux.DomainState
import com.tangem.domain.redux.state.StringActionStateConverter
import org.rekotlin.Action
@@ -64,8 +64,6 @@ data class AddCustomTokenState(
fun getError(id: FieldId): AddCustomTokenError? = formErrors[id]
- fun hasError(id: FieldId): Boolean = formErrors[id] != null
-
inline fun visitDataConverter(converter: FieldDataConverter): T {
form.visitDataConverter(converter)
return converter.getConvertedData()
@@ -116,9 +114,12 @@ data class AddCustomTokenState(
return network.data.value != Blockchain.Unknown
}
- fun getCustomTokenType(): CustomTokenType = when {
- tokensAnyFieldsIsFilled() || tokensFieldsIsFilled() -> CustomTokenType.Token
- else -> CustomTokenType.Blockchain
+ fun getCustomTokenType(): CustomTokenType {
+ return if (tokensAnyFieldsIsFilled() || tokensFieldsIsFilled()) {
+ CustomTokenType.Token
+ } else {
+ CustomTokenType.Blockchain
+ }
}
fun gatherUserToken(): CustomCurrency.CustomToken? = try {
@@ -159,10 +160,6 @@ data class AddCustomTokenState(
.getConvertedData()
}
- fun getNetworks(card: CardDTO, type: CustomTokenType): List {
- return getNetworksList(card, type)
- }
-
companion object {
/**
@@ -268,7 +265,7 @@ data class AddCustomTokenState(
private var builder: StringBuilder = StringBuilder()
override fun convert(action: Action, stateHolder: DomainState): String? {
- val action = action as? AddCustomTokenAction ?: return null
+ if (action !is AddCustomTokenAction) return null
val state = stateHolder.addCustomTokensState
val fieldConverter =
diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt b/domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt
rename to domain/legacy/src/main/java/com/tangem/domain/features/addCustomToken/redux/Models.kt
diff --git a/domain/src/main/java/com/tangem/domain/redux/DomainState.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/redux/DomainState.kt
rename to domain/legacy/src/main/java/com/tangem/domain/redux/DomainState.kt
diff --git a/domain/src/main/java/com/tangem/domain/redux/DomainStore.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/redux/DomainStore.kt
rename to domain/legacy/src/main/java/com/tangem/domain/redux/DomainStore.kt
diff --git a/domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/ReStoreHub.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/redux/ReStoreHub.kt
rename to domain/legacy/src/main/java/com/tangem/domain/redux/ReStoreHub.kt
diff --git a/domain/src/main/java/com/tangem/domain/redux/extensions/Dispatch.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/extensions/Dispatch.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/redux/extensions/Dispatch.kt
rename to domain/legacy/src/main/java/com/tangem/domain/redux/extensions/Dispatch.kt
diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt
similarity index 88%
rename from domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt
rename to domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt
index f4256b644e..2c9edb34a5 100644
--- a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalAction.kt
@@ -1,7 +1,7 @@
package com.tangem.domain.redux.global
import com.tangem.domain.DomainDialog
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
import org.rekotlin.Action
/**
diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt
rename to domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalHub.kt
diff --git a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt
similarity index 93%
rename from domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt
rename to domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt
index 231584ecf6..36a97859c9 100644
--- a/domain/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt
+++ b/domain/legacy/src/main/java/com/tangem/domain/redux/global/DomainGlobalState.kt
@@ -3,7 +3,7 @@ package com.tangem.domain.redux.global
import com.tangem.datasource.api.paymentology.PaymentologyApiService
import com.tangem.datasource.api.tangemTech.TangemTechService
import com.tangem.domain.DomainDialog
-import com.tangem.domain.common.ScanResponse
+import com.tangem.domain.models.scan.ScanResponse
/**
[REDACTED_AUTHOR]
diff --git a/domain/src/main/java/com/tangem/domain/redux/state/StateConverter.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateConverter.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/redux/state/StateConverter.kt
rename to domain/legacy/src/main/java/com/tangem/domain/redux/state/StateConverter.kt
diff --git a/domain/src/main/java/com/tangem/domain/redux/state/StateLogger.kt b/domain/legacy/src/main/java/com/tangem/domain/redux/state/StateLogger.kt
similarity index 100%
rename from domain/src/main/java/com/tangem/domain/redux/state/StateLogger.kt
rename to domain/legacy/src/main/java/com/tangem/domain/redux/state/StateLogger.kt
diff --git a/domain/src/test/java/com/tangem/domain/features/BlockchainTests.kt b/domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt
similarity index 100%
rename from domain/src/test/java/com/tangem/domain/features/BlockchainTests.kt
rename to domain/legacy/src/test/java/com/tangem/domain/features/BlockchainTests.kt
diff --git a/domain/models/.gitignore b/domain/models/.gitignore
new file mode 100644
index 0000000000..796b96d1c4
--- /dev/null
+++ b/domain/models/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/domain/models/build.gradle.kts b/domain/models/build.gradle.kts
new file mode 100644
index 0000000000..fbccb4120d
--- /dev/null
+++ b/domain/models/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ alias(deps.plugins.kotlin.jvm)
+ id("configuration")
+}
+
+dependencies {
+ implementation(deps.tangem.card.core)
+ implementation(deps.moshi.kotlin)
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/tangem/domain/common/CardDTO.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt
similarity index 95%
rename from domain/src/main/java/com/tangem/domain/common/CardDTO.kt
rename to domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt
index ffe33e01e2..fa9bb0ee84 100644
--- a/domain/src/main/java/com/tangem/domain/common/CardDTO.kt
+++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/CardDTO.kt
@@ -1,4 +1,4 @@
-package com.tangem.domain.common
+package com.tangem.domain.models.scan
import com.squareup.moshi.JsonClass
import com.tangem.common.card.Card
@@ -8,7 +8,7 @@ import com.tangem.common.card.EncryptionMode
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.operations.attestation.Attestation
-import java.util.*
+import java.util.Date
import com.tangem.common.card.FirmwareVersion as SdkFirmwareVersion
/**
@@ -23,6 +23,7 @@ data class CardDTO(
val manufacturer: Manufacturer,
val issuer: Issuer,
val settings: Settings,
+ val userSettings: UserSettings?,
val linkedTerminalStatus: LinkedTerminalStatus,
val isAccessCodeSet: Boolean,
val isPasscodeSet: Boolean?,
@@ -39,6 +40,7 @@ data class CardDTO(
manufacturer = Manufacturer(card.manufacturer),
issuer = Issuer(card.issuer),
settings = Settings(card.settings),
+ userSettings = UserSettings(card.userSettings),
linkedTerminalStatus = LinkedTerminalStatus.fromSdkStatus(card.linkedTerminalStatus),
isAccessCodeSet = card.isAccessCodeSet,
isPasscodeSet = card.isPasscodeSet,
@@ -60,6 +62,7 @@ data class CardDTO(
if (manufacturer != other.manufacturer) return false
if (issuer != other.issuer) return false
if (settings != other.settings) return false
+ if (userSettings != other.userSettings) return false
if (linkedTerminalStatus != other.linkedTerminalStatus) return false
if (isAccessCodeSet != other.isAccessCodeSet) return false
if (isPasscodeSet != other.isPasscodeSet) return false
@@ -78,6 +81,7 @@ data class CardDTO(
result = 31 * result + manufacturer.hashCode()
result = 31 * result + issuer.hashCode()
result = 31 * result + settings.hashCode()
+ result = 31 * result + userSettings.hashCode()
result = 31 * result + linkedTerminalStatus.hashCode()
result = 31 * result + isAccessCodeSet.hashCode()
result = 31 * result + (isPasscodeSet?.hashCode() ?: 0)
@@ -114,6 +118,15 @@ data class CardDTO(
)
}
+ @JsonClass(generateAdapter = true)
+ data class UserSettings(
+ val isUserCodeRecoveryAllowed: Boolean,
+ ) {
+ constructor(userSettings: com.tangem.common.card.UserSettings) : this(
+ userSettings.isUserCodeRecoveryAllowed,
+ )
+ }
+
@JsonClass(generateAdapter = true)
data class FirmwareVersion(
val major: Int,
diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt
new file mode 100644
index 0000000000..c9cae94a64
--- /dev/null
+++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/ScanResponse.kt
@@ -0,0 +1,24 @@
+package com.tangem.domain.models.scan
+
+import com.tangem.common.card.WalletData
+import com.tangem.common.extensions.ByteArrayKey
+import com.tangem.operations.backup.PrimaryCard
+import com.tangem.operations.derivation.ExtendedPublicKeysMap
+
+/**
+[REDACTED_AUTHOR]
+ */
+data class ScanResponse(
+ val card: CardDTO,
+ val productType: ProductType,
+ val walletData: WalletData?,
+ val secondTwinPublicKey: String? = null,
+ val derivedKeys: Map = mapOf(),
+ val primaryCard: PrimaryCard? = null,
+)
+
+typealias KeyWalletPublicKey = ByteArrayKey
+
+enum class ProductType {
+ Note, Twins, Wallet, SaltPay, Start2Coin
+}
\ No newline at end of file
diff --git a/domain/src/main/java/com/tangem/domain/common/CardInfo.kt b/domain/src/main/java/com/tangem/domain/common/CardInfo.kt
deleted file mode 100644
index 7953c475ac..0000000000
--- a/domain/src/main/java/com/tangem/domain/common/CardInfo.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package com.tangem.domain.common
-
-import com.tangem.common.card.WalletData
-import com.tangem.operations.backup.PrimaryCard
-import com.tangem.operations.derivation.ExtendedPublicKeysMap
-
-data class CardInfo(
- val card: CardDTO,
- val productType: ProductType,
- val walletData: WalletData?,
- val secondTwinPublicKey: String?,
- val derivedKeys: Map,
- val primaryCard: PrimaryCard?,
-)
\ 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
deleted file mode 100644
index a93b21fba7..0000000000
--- a/domain/src/main/java/com/tangem/domain/common/ScanResponse.kt
+++ /dev/null
@@ -1,63 +0,0 @@
-package com.tangem.domain.common
-
-import com.tangem.blockchain.common.Blockchain
-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.crypto.hdWallet.DerivationPath
-import com.tangem.domain.common.TapWorkarounds.isTangemTwins
-import com.tangem.domain.common.TapWorkarounds.isTestCard
-import com.tangem.operations.CommandResponse
-import com.tangem.operations.backup.PrimaryCard
-import com.tangem.operations.derivation.ExtendedPublicKeysMap
-
-/**
-[REDACTED_AUTHOR]
- */
-data class ScanResponse(
- val card: CardDTO,
- val productType: ProductType,
- val walletData: WalletData?,
- val secondTwinPublicKey: String? = null,
- val derivedKeys: Map = mapOf(),
- val primaryCard: PrimaryCard? = null,
-) : CommandResponse {
-
- val cardTypesResolver: CardTypesResolver = TangemCardTypesResolver(
- card = card,
- productType = productType,
- walletData = walletData,
- )
-
- fun twinsIsTwinned(): Boolean = card.isTangemTwins && walletData != null && secondTwinPublicKey != null
- fun supportsHdWallet(): Boolean = card.settings.isHDWalletAllowed
- fun supportsBackup(): Boolean = card.settings.isBackupAllowed
-
- fun hasDerivation(blockchain: Blockchain, rawDerivationPath: String): Boolean {
- return hasDerivation(blockchain, DerivationPath(rawDerivationPath))
- }
-
- private 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
- }
- }
-
- private 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
- }
-}
-
-typealias KeyWalletPublicKey = ByteArrayKey
\ No newline at end of file
diff --git a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt b/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt
deleted file mode 100644
index 2296da1914..0000000000
--- a/domain/src/main/java/com/tangem/domain/features/addCustomToken/AddCustomTokenService.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.tangem.domain.features.addCustomToken
-
-import com.tangem.datasource.api.tangemTech.TangemTechApi
-import com.tangem.datasource.api.tangemTech.models.CoinsResponse
-import com.tangem.utils.coroutines.CoroutineDispatcherProvider
-import kotlinx.coroutines.withContext
-
-/**
-[REDACTED_AUTHOR]
- */
-class AddCustomTokenService(
- private val tangemTechApi: TangemTechApi,
- private val dispatchers: CoroutineDispatcherProvider,
- private val supportedTokenNetworkIds: List,
-) {
-
- suspend fun findToken(
- contractAddress: String,
- networkId: String? = null,
- active: Boolean? = null,
- ): List = withContext(dispatchers.io) {
- runCatching {
- tangemTechApi.getCoins(
- contractAddress = contractAddress,
- networkIds = selectNetworksForSearch(networkId),
- active = active,
- )
- }
- .onSuccess { response ->
- var coinsList = mutableListOf()
- response.coins.forEach { coin ->
- val networksWithTheSameAddress = coin.networks
- .filter { it.contractAddress != null || it.decimalCount != null }
- .filter { it.contractAddress?.equals(contractAddress, ignoreCase = true) == true }
- .filter { supportedTokenNetworkIds.contains(it.networkId) }
- if (networksWithTheSameAddress.isNotEmpty()) {
- val newToken = coin.copy(networks = networksWithTheSameAddress)
- coinsList.add(newToken)
- }
- }
- if (coinsList.size > 1) {
- // https://tangem.slack.com/archives/GMXC6PP71/p1649672562078679
- coinsList = mutableListOf(coinsList[0])
- }
- return@withContext coinsList
- }
- .onFailure {
- return@withContext emptyList()
- }
-
- error("Unreachable code because runCatching must return result")
- }
-
- private fun selectNetworksForSearch(networkId: String?): String {
- return networkId ?: supportedTokenNetworkIds.joinToString(",")
- }
-}
\ No newline at end of file
diff --git a/features/onboarding/.gitignore b/features/onboarding/.gitignore
new file mode 100644
index 0000000000..42afabfd2a
--- /dev/null
+++ b/features/onboarding/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/features/onboarding/build.gradle.kts b/features/onboarding/build.gradle.kts
new file mode 100644
index 0000000000..61df104ad9
--- /dev/null
+++ b/features/onboarding/build.gradle.kts
@@ -0,0 +1,50 @@
+plugins {
+ alias(deps.plugins.android.library)
+ alias(deps.plugins.kotlin.android)
+ alias(deps.plugins.kotlin.kapt)
+ alias(deps.plugins.kotlin.serialization)
+ alias(deps.plugins.hilt.android)
+ id("configuration")
+}
+
+dependencies {
+ /** Core modules */
+ implementation(project(":common"))
+ implementation(project(":core:featuretoggles"))
+ implementation(project(":core:datasource"))
+ implementation(project(":core:analytics"))
+ implementation(project(":core:utils"))
+ implementation(project(":core:ui"))
+ implementation(project(":core:res"))
+
+ /** AndroidX libraries */
+ implementation(deps.androidx.core.ktx)
+ implementation(deps.androidx.appCompat)
+ implementation(deps.androidx.fragment.ktx)
+ implementation(deps.androidx.constraintLayout)
+ implementation(deps.androidx.activity.compose)
+ implementation(deps.lifecycle.runtime.ktx)
+ implementation(deps.lifecycle.common.java8)
+ implementation(deps.lifecycle.viewModel.ktx)
+
+ /** Compose libraries */
+ implementation(deps.compose.material)
+ implementation(deps.compose.animation)
+ implementation(deps.compose.foundation)
+ implementation(deps.compose.ui)
+ implementation(deps.compose.ui.tooling)
+ implementation(deps.compose.coil)
+ implementation(deps.compose.shimmer)
+
+ implementation(deps.tangem.card.core)
+
+ /** Other libraries */
+ implementation(deps.compose.shimmer)
+ implementation(deps.kotlin.immutable.collections)
+ implementation(deps.kotlin.serialization)
+ implementation(deps.timber)
+
+ /** DI */
+ implementation(deps.hilt.android)
+ kapt(deps.hilt.kapt)
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/AndroidManifest.xml b/features/onboarding/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..21015a0f53
--- /dev/null
+++ b/features/onboarding/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
+
\ No newline at end of file
diff --git a/features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt b/features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt
new file mode 100644
index 0000000000..f78ccaf2ce
--- /dev/null
+++ b/features/onboarding/src/main/assets/mnemonic/mnemonic_dictionary_en.txt
@@ -0,0 +1,2048 @@
+abandon
+ability
+able
+about
+above
+absent
+absorb
+abstract
+absurd
+abuse
+access
+accident
+account
+accuse
+achieve
+acid
+acoustic
+acquire
+across
+act
+action
+actor
+actress
+actual
+adapt
+add
+addict
+address
+adjust
+admit
+adult
+advance
+advice
+aerobic
+affair
+afford
+afraid
+again
+age
+agent
+agree
+ahead
+aim
+air
+airport
+aisle
+alarm
+album
+alcohol
+alert
+alien
+all
+alley
+allow
+almost
+alone
+alpha
+already
+also
+alter
+always
+amateur
+amazing
+among
+amount
+amused
+analyst
+anchor
+ancient
+anger
+angle
+angry
+animal
+ankle
+announce
+annual
+another
+answer
+antenna
+antique
+anxiety
+any
+apart
+apology
+appear
+apple
+approve
+april
+arch
+arctic
+area
+arena
+argue
+arm
+armed
+armor
+army
+around
+arrange
+arrest
+arrive
+arrow
+art
+artefact
+artist
+artwork
+ask
+aspect
+assault
+asset
+assist
+assume
+asthma
+athlete
+atom
+attack
+attend
+attitude
+attract
+auction
+audit
+august
+aunt
+author
+auto
+autumn
+average
+avocado
+avoid
+awake
+aware
+away
+awesome
+awful
+awkward
+axis
+baby
+bachelor
+bacon
+badge
+bag
+balance
+balcony
+ball
+bamboo
+banana
+banner
+bar
+barely
+bargain
+barrel
+base
+basic
+basket
+battle
+beach
+bean
+beauty
+because
+become
+beef
+before
+begin
+behave
+behind
+believe
+below
+belt
+bench
+benefit
+best
+betray
+better
+between
+beyond
+bicycle
+bid
+bike
+bind
+biology
+bird
+birth
+bitter
+black
+blade
+blame
+blanket
+blast
+bleak
+bless
+blind
+blood
+blossom
+blouse
+blue
+blur
+blush
+board
+boat
+body
+boil
+bomb
+bone
+bonus
+book
+boost
+border
+boring
+borrow
+boss
+bottom
+bounce
+box
+boy
+bracket
+brain
+brand
+brass
+brave
+bread
+breeze
+brick
+bridge
+brief
+bright
+bring
+brisk
+broccoli
+broken
+bronze
+broom
+brother
+brown
+brush
+bubble
+buddy
+budget
+buffalo
+build
+bulb
+bulk
+bullet
+bundle
+bunker
+burden
+burger
+burst
+bus
+business
+busy
+butter
+buyer
+buzz
+cabbage
+cabin
+cable
+cactus
+cage
+cake
+call
+calm
+camera
+camp
+can
+canal
+cancel
+candy
+cannon
+canoe
+canvas
+canyon
+capable
+capital
+captain
+car
+carbon
+card
+cargo
+carpet
+carry
+cart
+case
+cash
+casino
+castle
+casual
+cat
+catalog
+catch
+category
+cattle
+caught
+cause
+caution
+cave
+ceiling
+celery
+cement
+census
+century
+cereal
+certain
+chair
+chalk
+champion
+change
+chaos
+chapter
+charge
+chase
+chat
+cheap
+check
+cheese
+chef
+cherry
+chest
+chicken
+chief
+child
+chimney
+choice
+choose
+chronic
+chuckle
+chunk
+churn
+cigar
+cinnamon
+circle
+citizen
+city
+civil
+claim
+clap
+clarify
+claw
+clay
+clean
+clerk
+clever
+click
+client
+cliff
+climb
+clinic
+clip
+clock
+clog
+close
+cloth
+cloud
+clown
+club
+clump
+cluster
+clutch
+coach
+coast
+coconut
+code
+coffee
+coil
+coin
+collect
+color
+column
+combine
+come
+comfort
+comic
+common
+company
+concert
+conduct
+confirm
+congress
+connect
+consider
+control
+convince
+cook
+cool
+copper
+copy
+coral
+core
+corn
+correct
+cost
+cotton
+couch
+country
+couple
+course
+cousin
+cover
+coyote
+crack
+cradle
+craft
+cram
+crane
+crash
+crater
+crawl
+crazy
+cream
+credit
+creek
+crew
+cricket
+crime
+crisp
+critic
+crop
+cross
+crouch
+crowd
+crucial
+cruel
+cruise
+crumble
+crunch
+crush
+cry
+crystal
+cube
+culture
+cup
+cupboard
+curious
+current
+curtain
+curve
+cushion
+custom
+cute
+cycle
+dad
+damage
+damp
+dance
+danger
+daring
+dash
+daughter
+dawn
+day
+deal
+debate
+debris
+decade
+december
+decide
+decline
+decorate
+decrease
+deer
+defense
+define
+defy
+degree
+delay
+deliver
+demand
+demise
+denial
+dentist
+deny
+depart
+depend
+deposit
+depth
+deputy
+derive
+describe
+desert
+design
+desk
+despair
+destroy
+detail
+detect
+develop
+device
+devote
+diagram
+dial
+diamond
+diary
+dice
+diesel
+diet
+differ
+digital
+dignity
+dilemma
+dinner
+dinosaur
+direct
+dirt
+disagree
+discover
+disease
+dish
+dismiss
+disorder
+display
+distance
+divert
+divide
+divorce
+dizzy
+doctor
+document
+dog
+doll
+dolphin
+domain
+donate
+donkey
+donor
+door
+dose
+double
+dove
+draft
+dragon
+drama
+drastic
+draw
+dream
+dress
+drift
+drill
+drink
+drip
+drive
+drop
+drum
+dry
+duck
+dumb
+dune
+during
+dust
+dutch
+duty
+dwarf
+dynamic
+eager
+eagle
+early
+earn
+earth
+easily
+east
+easy
+echo
+ecology
+economy
+edge
+edit
+educate
+effort
+egg
+eight
+either
+elbow
+elder
+electric
+elegant
+element
+elephant
+elevator
+elite
+else
+embark
+embody
+embrace
+emerge
+emotion
+employ
+empower
+empty
+enable
+enact
+end
+endless
+endorse
+enemy
+energy
+enforce
+engage
+engine
+enhance
+enjoy
+enlist
+enough
+enrich
+enroll
+ensure
+enter
+entire
+entry
+envelope
+episode
+equal
+equip
+era
+erase
+erode
+erosion
+error
+erupt
+escape
+essay
+essence
+estate
+eternal
+ethics
+evidence
+evil
+evoke
+evolve
+exact
+example
+excess
+exchange
+excite
+exclude
+excuse
+execute
+exercise
+exhaust
+exhibit
+exile
+exist
+exit
+exotic
+expand
+expect
+expire
+explain
+expose
+express
+extend
+extra
+eye
+eyebrow
+fabric
+face
+faculty
+fade
+faint
+faith
+fall
+false
+fame
+family
+famous
+fan
+fancy
+fantasy
+farm
+fashion
+fat
+fatal
+father
+fatigue
+fault
+favorite
+feature
+february
+federal
+fee
+feed
+feel
+female
+fence
+festival
+fetch
+fever
+few
+fiber
+fiction
+field
+figure
+file
+film
+filter
+final
+find
+fine
+finger
+finish
+fire
+firm
+first
+fiscal
+fish
+fit
+fitness
+fix
+flag
+flame
+flash
+flat
+flavor
+flee
+flight
+flip
+float
+flock
+floor
+flower
+fluid
+flush
+fly
+foam
+focus
+fog
+foil
+fold
+follow
+food
+foot
+force
+forest
+forget
+fork
+fortune
+forum
+forward
+fossil
+foster
+found
+fox
+fragile
+frame
+frequent
+fresh
+friend
+fringe
+frog
+front
+frost
+frown
+frozen
+fruit
+fuel
+fun
+funny
+furnace
+fury
+future
+gadget
+gain
+galaxy
+gallery
+game
+gap
+garage
+garbage
+garden
+garlic
+garment
+gas
+gasp
+gate
+gather
+gauge
+gaze
+general
+genius
+genre
+gentle
+genuine
+gesture
+ghost
+giant
+gift
+giggle
+ginger
+giraffe
+girl
+give
+glad
+glance
+glare
+glass
+glide
+glimpse
+globe
+gloom
+glory
+glove
+glow
+glue
+goat
+goddess
+gold
+good
+goose
+gorilla
+gospel
+gossip
+govern
+gown
+grab
+grace
+grain
+grant
+grape
+grass
+gravity
+great
+green
+grid
+grief
+grit
+grocery
+group
+grow
+grunt
+guard
+guess
+guide
+guilt
+guitar
+gun
+gym
+habit
+hair
+half
+hammer
+hamster
+hand
+happy
+harbor
+hard
+harsh
+harvest
+hat
+have
+hawk
+hazard
+head
+health
+heart
+heavy
+hedgehog
+height
+hello
+helmet
+help
+hen
+hero
+hidden
+high
+hill
+hint
+hip
+hire
+history
+hobby
+hockey
+hold
+hole
+holiday
+hollow
+home
+honey
+hood
+hope
+horn
+horror
+horse
+hospital
+host
+hotel
+hour
+hover
+hub
+huge
+human
+humble
+humor
+hundred
+hungry
+hunt
+hurdle
+hurry
+hurt
+husband
+hybrid
+ice
+icon
+idea
+identify
+idle
+ignore
+ill
+illegal
+illness
+image
+imitate
+immense
+immune
+impact
+impose
+improve
+impulse
+inch
+include
+income
+increase
+index
+indicate
+indoor
+industry
+infant
+inflict
+inform
+inhale
+inherit
+initial
+inject
+injury
+inmate
+inner
+innocent
+input
+inquiry
+insane
+insect
+inside
+inspire
+install
+intact
+interest
+into
+invest
+invite
+involve
+iron
+island
+isolate
+issue
+item
+ivory
+jacket
+jaguar
+jar
+jazz
+jealous
+jeans
+jelly
+jewel
+job
+join
+joke
+journey
+joy
+judge
+juice
+jump
+jungle
+junior
+junk
+just
+kangaroo
+keen
+keep
+ketchup
+key
+kick
+kid
+kidney
+kind
+kingdom
+kiss
+kit
+kitchen
+kite
+kitten
+kiwi
+knee
+knife
+knock
+know
+lab
+label
+labor
+ladder
+lady
+lake
+lamp
+language
+laptop
+large
+later
+latin
+laugh
+laundry
+lava
+law
+lawn
+lawsuit
+layer
+lazy
+leader
+leaf
+learn
+leave
+lecture
+left
+leg
+legal
+legend
+leisure
+lemon
+lend
+length
+lens
+leopard
+lesson
+letter
+level
+liar
+liberty
+library
+license
+life
+lift
+light
+like
+limb
+limit
+link
+lion
+liquid
+list
+little
+live
+lizard
+load
+loan
+lobster
+local
+lock
+logic
+lonely
+long
+loop
+lottery
+loud
+lounge
+love
+loyal
+lucky
+luggage
+lumber
+lunar
+lunch
+luxury
+lyrics
+machine
+mad
+magic
+magnet
+maid
+mail
+main
+major
+make
+mammal
+man
+manage
+mandate
+mango
+mansion
+manual
+maple
+marble
+march
+margin
+marine
+market
+marriage
+mask
+mass
+master
+match
+material
+math
+matrix
+matter
+maximum
+maze
+meadow
+mean
+measure
+meat
+mechanic
+medal
+media
+melody
+melt
+member
+memory
+mention
+menu
+mercy
+merge
+merit
+merry
+mesh
+message
+metal
+method
+middle
+midnight
+milk
+million
+mimic
+mind
+minimum
+minor
+minute
+miracle
+mirror
+misery
+miss
+mistake
+mix
+mixed
+mixture
+mobile
+model
+modify
+mom
+moment
+monitor
+monkey
+monster
+month
+moon
+moral
+more
+morning
+mosquito
+mother
+motion
+motor
+mountain
+mouse
+move
+movie
+much
+muffin
+mule
+multiply
+muscle
+museum
+mushroom
+music
+must
+mutual
+myself
+mystery
+myth
+naive
+name
+napkin
+narrow
+nasty
+nation
+nature
+near
+neck
+need
+negative
+neglect
+neither
+nephew
+nerve
+nest
+net
+network
+neutral
+never
+news
+next
+nice
+night
+noble
+noise
+nominee
+noodle
+normal
+north
+nose
+notable
+note
+nothing
+notice
+novel
+now
+nuclear
+number
+nurse
+nut
+oak
+obey
+object
+oblige
+obscure
+observe
+obtain
+obvious
+occur
+ocean
+october
+odor
+off
+offer
+office
+often
+oil
+okay
+old
+olive
+olympic
+omit
+once
+one
+onion
+online
+only
+open
+opera
+opinion
+oppose
+option
+orange
+orbit
+orchard
+order
+ordinary
+organ
+orient
+original
+orphan
+ostrich
+other
+outdoor
+outer
+output
+outside
+oval
+oven
+over
+own
+owner
+oxygen
+oyster
+ozone
+pact
+paddle
+page
+pair
+palace
+palm
+panda
+panel
+panic
+panther
+paper
+parade
+parent
+park
+parrot
+party
+pass
+patch
+path
+patient
+patrol
+pattern
+pause
+pave
+payment
+peace
+peanut
+pear
+peasant
+pelican
+pen
+penalty
+pencil
+people
+pepper
+perfect
+permit
+person
+pet
+phone
+photo
+phrase
+physical
+piano
+picnic
+picture
+piece
+pig
+pigeon
+pill
+pilot
+pink
+pioneer
+pipe
+pistol
+pitch
+pizza
+place
+planet
+plastic
+plate
+play
+please
+pledge
+pluck
+plug
+plunge
+poem
+poet
+point
+polar
+pole
+police
+pond
+pony
+pool
+popular
+portion
+position
+possible
+post
+potato
+pottery
+poverty
+powder
+power
+practice
+praise
+predict
+prefer
+prepare
+present
+pretty
+prevent
+price
+pride
+primary
+print
+priority
+prison
+private
+prize
+problem
+process
+produce
+profit
+program
+project
+promote
+proof
+property
+prosper
+protect
+proud
+provide
+public
+pudding
+pull
+pulp
+pulse
+pumpkin
+punch
+pupil
+puppy
+purchase
+purity
+purpose
+purse
+push
+put
+puzzle
+pyramid
+quality
+quantum
+quarter
+question
+quick
+quit
+quiz
+quote
+rabbit
+raccoon
+race
+rack
+radar
+radio
+rail
+rain
+raise
+rally
+ramp
+ranch
+random
+range
+rapid
+rare
+rate
+rather
+raven
+raw
+razor
+ready
+real
+reason
+rebel
+rebuild
+recall
+receive
+recipe
+record
+recycle
+reduce
+reflect
+reform
+refuse
+region
+regret
+regular
+reject
+relax
+release
+relief
+rely
+remain
+remember
+remind
+remove
+render
+renew
+rent
+reopen
+repair
+repeat
+replace
+report
+require
+rescue
+resemble
+resist
+resource
+response
+result
+retire
+retreat
+return
+reunion
+reveal
+review
+reward
+rhythm
+rib
+ribbon
+rice
+rich
+ride
+ridge
+rifle
+right
+rigid
+ring
+riot
+ripple
+risk
+ritual
+rival
+river
+road
+roast
+robot
+robust
+rocket
+romance
+roof
+rookie
+room
+rose
+rotate
+rough
+round
+route
+royal
+rubber
+rude
+rug
+rule
+run
+runway
+rural
+sad
+saddle
+sadness
+safe
+sail
+salad
+salmon
+salon
+salt
+salute
+same
+sample
+sand
+satisfy
+satoshi
+sauce
+sausage
+save
+say
+scale
+scan
+scare
+scatter
+scene
+scheme
+school
+science
+scissors
+scorpion
+scout
+scrap
+screen
+script
+scrub
+sea
+search
+season
+seat
+second
+secret
+section
+security
+seed
+seek
+segment
+select
+sell
+seminar
+senior
+sense
+sentence
+series
+service
+session
+settle
+setup
+seven
+shadow
+shaft
+shallow
+share
+shed
+shell
+sheriff
+shield
+shift
+shine
+ship
+shiver
+shock
+shoe
+shoot
+shop
+short
+shoulder
+shove
+shrimp
+shrug
+shuffle
+shy
+sibling
+sick
+side
+siege
+sight
+sign
+silent
+silk
+silly
+silver
+similar
+simple
+since
+sing
+siren
+sister
+situate
+six
+size
+skate
+sketch
+ski
+skill
+skin
+skirt
+skull
+slab
+slam
+sleep
+slender
+slice
+slide
+slight
+slim
+slogan
+slot
+slow
+slush
+small
+smart
+smile
+smoke
+smooth
+snack
+snake
+snap
+sniff
+snow
+soap
+soccer
+social
+sock
+soda
+soft
+solar
+soldier
+solid
+solution
+solve
+someone
+song
+soon
+sorry
+sort
+soul
+sound
+soup
+source
+south
+space
+spare
+spatial
+spawn
+speak
+special
+speed
+spell
+spend
+sphere
+spice
+spider
+spike
+spin
+spirit
+split
+spoil
+sponsor
+spoon
+sport
+spot
+spray
+spread
+spring
+spy
+square
+squeeze
+squirrel
+stable
+stadium
+staff
+stage
+stairs
+stamp
+stand
+start
+state
+stay
+steak
+steel
+stem
+step
+stereo
+stick
+still
+sting
+stock
+stomach
+stone
+stool
+story
+stove
+strategy
+street
+strike
+strong
+struggle
+student
+stuff
+stumble
+style
+subject
+submit
+subway
+success
+such
+sudden
+suffer
+sugar
+suggest
+suit
+summer
+sun
+sunny
+sunset
+super
+supply
+supreme
+sure
+surface
+surge
+surprise
+surround
+survey
+suspect
+sustain
+swallow
+swamp
+swap
+swarm
+swear
+sweet
+swift
+swim
+swing
+switch
+sword
+symbol
+symptom
+syrup
+system
+table
+tackle
+tag
+tail
+talent
+talk
+tank
+tape
+target
+task
+taste
+tattoo
+taxi
+teach
+team
+tell
+ten
+tenant
+tennis
+tent
+term
+test
+text
+thank
+that
+theme
+then
+theory
+there
+they
+thing
+this
+thought
+three
+thrive
+throw
+thumb
+thunder
+ticket
+tide
+tiger
+tilt
+timber
+time
+tiny
+tip
+tired
+tissue
+title
+toast
+tobacco
+today
+toddler
+toe
+together
+toilet
+token
+tomato
+tomorrow
+tone
+tongue
+tonight
+tool
+tooth
+top
+topic
+topple
+torch
+tornado
+tortoise
+toss
+total
+tourist
+toward
+tower
+town
+toy
+track
+trade
+traffic
+tragic
+train
+transfer
+trap
+trash
+travel
+tray
+treat
+tree
+trend
+trial
+tribe
+trick
+trigger
+trim
+trip
+trophy
+trouble
+truck
+true
+truly
+trumpet
+trust
+truth
+try
+tube
+tuition
+tumble
+tuna
+tunnel
+turkey
+turn
+turtle
+twelve
+twenty
+twice
+twin
+twist
+two
+type
+typical
+ugly
+umbrella
+unable
+unaware
+uncle
+uncover
+under
+undo
+unfair
+unfold
+unhappy
+uniform
+unique
+unit
+universe
+unknown
+unlock
+until
+unusual
+unveil
+update
+upgrade
+uphold
+upon
+upper
+upset
+urban
+urge
+usage
+use
+used
+useful
+useless
+usual
+utility
+vacant
+vacuum
+vague
+valid
+valley
+valve
+van
+vanish
+vapor
+various
+vast
+vault
+vehicle
+velvet
+vendor
+venture
+venue
+verb
+verify
+version
+very
+vessel
+veteran
+viable
+vibrant
+vicious
+victory
+video
+view
+village
+vintage
+violin
+virtual
+virus
+visa
+visit
+visual
+vital
+vivid
+vocal
+voice
+void
+volcano
+volume
+vote
+voyage
+wage
+wagon
+wait
+walk
+wall
+walnut
+want
+warfare
+warm
+warrior
+wash
+wasp
+waste
+water
+wave
+way
+wealth
+weapon
+wear
+weasel
+weather
+web
+wedding
+weekend
+weird
+welcome
+west
+wet
+whale
+what
+wheat
+wheel
+when
+where
+whip
+whisper
+wide
+width
+wife
+wild
+will
+win
+window
+wine
+wing
+wink
+winner
+winter
+wire
+wisdom
+wise
+wish
+witness
+wolf
+woman
+wonder
+wood
+wool
+word
+work
+world
+worry
+worth
+wrap
+wreck
+wrestle
+wrist
+write
+wrong
+yard
+year
+yellow
+you
+young
+youth
+zebra
+zero
+zone
+zoo
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt
new file mode 100644
index 0000000000..116b7239c3
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/DefaultMnemonicRepository.kt
@@ -0,0 +1,32 @@
+package com.tangem.feature.onboarding.data
+
+import com.tangem.crypto.bip39.*
+import com.tangem.datasource.asset.AssetReader
+
+/**
+[REDACTED_AUTHOR]
+ */
+internal class DefaultMnemonicRepository(
+ private val assetReader: AssetReader,
+) : MnemonicRepository {
+
+ // TODO: change initialization after adding it to the sdk
+ private val bip39Wordlist: Wordlist by lazy {
+ assetReader.openFile(MNEMONIC_FILE_NAME)
+ .use { BIP39Wordlist(it) }
+ }
+
+ private val bip39Words: Set by lazy { bip39Wordlist.words.toHashSet() }
+
+ override fun getWordsDictionary(): Set = bip39Words
+
+ override fun generateDefaultMnemonic(): Mnemonic {
+ return DefaultMnemonic(EntropyLength.Bits128Length, bip39Wordlist)
+ }
+
+ override fun createMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(mnemonicString, bip39Wordlist)
+
+ companion object {
+ private const val MNEMONIC_FILE_NAME = "mnemonic/mnemonic_dictionary_en.txt"
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt
new file mode 100644
index 0000000000..b1b7ebfa2c
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/MnemonicRepository.kt
@@ -0,0 +1,18 @@
+package com.tangem.feature.onboarding.data
+
+import com.tangem.common.core.TangemSdkError
+import com.tangem.crypto.bip39.Mnemonic
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface MnemonicRepository {
+
+ fun getWordsDictionary(): Set
+
+ @Throws(TangemSdkError.MnemonicException::class)
+ fun generateDefaultMnemonic(): Mnemonic
+
+ @Throws(TangemSdkError.MnemonicException::class)
+ fun createMnemonic(mnemonicString: String): Mnemonic
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt
new file mode 100644
index 0000000000..fe7e292403
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/data/di/OnboardingDataModule.kt
@@ -0,0 +1,21 @@
+package com.tangem.feature.onboarding.data.di
+
+import com.tangem.datasource.asset.AssetReader
+import com.tangem.feature.onboarding.data.DefaultMnemonicRepository
+import com.tangem.feature.onboarding.data.MnemonicRepository
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class OnboardingDataModule {
+
+ @Provides
+ @Singleton
+ fun provideSeedPhraseSdkRepository(assetReader: AssetReader): MnemonicRepository {
+ return DefaultMnemonicRepository(assetReader)
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt
new file mode 100644
index 0000000000..d0cd349b55
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/DefaultSeedPhraseInteractor.kt
@@ -0,0 +1,107 @@
+package com.tangem.feature.onboarding.domain
+
+import com.tangem.common.core.TangemSdkError
+import com.tangem.crypto.bip39.Mnemonic
+import com.tangem.crypto.bip39.MnemonicErrorResult
+import com.tangem.feature.onboarding.data.MnemonicRepository
+import com.tangem.utils.extensions.isNotWhitespace
+
+/**
+[REDACTED_AUTHOR]
+ */
+internal class DefaultSeedPhraseInteractor constructor(
+ private val repository: MnemonicRepository,
+) : SeedPhraseInteractor {
+
+ private var currentMnemonic: Mnemonic? = null
+ private val partWordFinder: PartWordFinder = PartWordFinder()
+
+ override suspend fun generateMnemonic(): Result {
+ return runCatching {
+ repository.generateDefaultMnemonic().apply {
+ currentMnemonic = this
+ }
+ }
+ }
+
+ override suspend fun getMnemonicComponents(): Result> {
+ return runCatching {
+ val mnemonic = currentMnemonic ?: repository.generateDefaultMnemonic().apply {
+ currentMnemonic = this
+ }
+ mnemonic.mnemonicComponents
+ }
+ }
+
+ override suspend fun isWordMatch(word: String): Boolean {
+ return repository.getWordsDictionary().contains(word)
+ }
+
+ override suspend fun validateMnemonicString(text: String): Result> {
+ val inputWords = text.split("\\s+".toRegex()).toSet()
+ val wordsDictionary = repository.getWordsDictionary()
+
+ val invalidWords = inputWords
+ .filter { it.isNotEmpty() && !wordsDictionary.contains(it) }
+ .toSet()
+
+ return if (invalidWords.isNotEmpty()) {
+ Result.failure(SeedPhraseError.InvalidWords(invalidWords))
+ } else {
+ try {
+ val mnemonic = repository.createMnemonic(text)
+ Result.success(mnemonic.mnemonicComponents)
+ } catch (ex: TangemSdkError.MnemonicException) {
+ val error = ex.mnemonicResult.mapToError()
+ Result.failure(error)
+ }
+ }
+ }
+
+ override suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List {
+ if (text.isEmpty() || cursorPosition == 0 || hasSelection) return emptyList()
+ val word = partWordFinder.getLeadPartOfWord(text, cursorPosition)
+ ?: return emptyList()
+
+ val suggestions = repository.getWordsDictionary()
+ .filter { it.startsWith(word, ignoreCase = false) && it != word }
+ .toList()
+
+ return suggestions
+ }
+
+ override suspend fun insertSuggestionWord(
+ text: String,
+ suggestion: String,
+ cursorPosition: Int,
+ ): InsertSuggestionResult {
+ val textPartLead = text.substring(0, cursorPosition)
+ val textPartLast = text.substring(cursorPosition, text.length)
+ val wordPartLast = partWordFinder.getLastPartOfWord(text, suggestion, cursorPosition)
+
+ val textBuilder = StringBuilder()
+ .append(textPartLead)
+ .append(wordPartLast)
+
+ if (textPartLast.isEmpty() || textPartLast[0].isNotWhitespace()) {
+ textBuilder.append(SeedPhraseInteractor.MNEMONIC_DELIMITER)
+ }
+ textBuilder.append(textPartLast)
+
+ val textWithSuggestion = textBuilder.toString()
+ val newCursorPosition = cursorPosition + wordPartLast.length + SeedPhraseInteractor.MNEMONIC_DELIMITER.length
+
+ return InsertSuggestionResult(textWithSuggestion, newCursorPosition)
+ }
+}
+
+private fun MnemonicErrorResult.mapToError(): SeedPhraseError = when (this) {
+ MnemonicErrorResult.InvalidWordCount -> SeedPhraseError.InvalidEntropyLength
+ MnemonicErrorResult.InvalidEntropyLength -> SeedPhraseError.InvalidEntropyLength
+ MnemonicErrorResult.InvalidWordsFile -> SeedPhraseError.InvalidWordsFile
+ MnemonicErrorResult.InvalidChecksum -> SeedPhraseError.InvalidChecksum
+ MnemonicErrorResult.MnenmonicCreationFailed -> SeedPhraseError.MnenmonicCreationFailed
+ MnemonicErrorResult.NormalizationFailed -> SeedPhraseError.NormalizationFailed
+ MnemonicErrorResult.UnsupportedLanguage -> SeedPhraseError.UnsupportedLanguage
+ is MnemonicErrorResult.InvalidWords -> SeedPhraseError.InvalidWords(this.words)
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt
new file mode 100644
index 0000000000..60a22f0fec
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/OnboardingModuleError.kt
@@ -0,0 +1,40 @@
+package com.tangem.feature.onboarding.domain
+
+import com.tangem.common.module.ModuleError
+import com.tangem.common.module.ModuleErrorCode
+import com.tangem.common.module.ModuleMessage
+
+/**
+[REDACTED_AUTHOR]
+ * For each pool of errors, use the codes lying in range 100 (200..299, 300..399, etc)
+ */
+abstract class OnboardingModuleError(
+ subCode: Int,
+ override val message: String,
+ override val data: Any?,
+) : ModuleMessage, ModuleError() {
+ override val code: Int = ModuleErrorCode.ONBOARDING + subCode
+
+ companion object {
+ internal const val ERROR_CODE_SEED_PHRASE = 100
+ }
+}
+
+sealed class SeedPhraseError(
+ subCode: Int = 0,
+ message: String? = null,
+ data: Any? = null,
+) : OnboardingModuleError(
+ subCode = ERROR_CODE_SEED_PHRASE + subCode,
+ message = message ?: this::class.java.simpleName,
+ data = data,
+) {
+ object InvalidWordCount : SeedPhraseError(subCode = 0)
+ object InvalidEntropyLength : SeedPhraseError(subCode = 1)
+ object InvalidWordsFile : SeedPhraseError(subCode = 2)
+ object InvalidChecksum : SeedPhraseError(subCode = 3)
+ object MnenmonicCreationFailed : SeedPhraseError(subCode = 4)
+ object NormalizationFailed : SeedPhraseError(subCode = 5)
+ object UnsupportedLanguage : SeedPhraseError(subCode = 6)
+ data class InvalidWords(val words: Set) : SeedPhraseError(subCode = 7)
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt
new file mode 100644
index 0000000000..97ca9cd8d9
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/PartWordFinder.kt
@@ -0,0 +1,50 @@
+package com.tangem.feature.onboarding.domain
+
+import com.tangem.utils.extensions.isNotWhitespace
+
+/**
+[REDACTED_AUTHOR]
+ * Class which provides helper functions to determine lead or last part of words for suggestions
+ */
+internal class PartWordFinder {
+
+ /**
+ * Searches and return part of a word on the left side of the [cursorPosition] into the [text]
+ */
+ fun getLeadPartOfWord(text: String, cursorPosition: Int): String? {
+ val charText = text.toCharArray().asList()
+ val cursorIsOutOfTextRange = cursorPosition > charText.size || cursorPosition < 0
+ val cursorAtStartOfText = cursorPosition == 0
+ val hadWhiteSpaceOnLeftOfCursor = charText[cursorPosition - 1].isWhitespace()
+ val cursorInMiddleOfWord = charText.size > cursorPosition && charText[cursorPosition].isNotWhitespace()
+
+ @Suppress("ComplexCondition")
+ if (cursorIsOutOfTextRange || cursorAtStartOfText || hadWhiteSpaceOnLeftOfCursor || cursorInMiddleOfWord) {
+ return null
+ }
+
+ for (charIndex in cursorPosition - 1 downTo 0) {
+ val char = charText[charIndex]
+ when {
+ charIndex == 0 -> {
+ return text.substring(charIndex, cursorPosition)
+ }
+ char.isWhitespace() -> {
+ return text.substring(charIndex + 1, cursorPosition)
+ }
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Returns a substring of the [word] which excludes the first word part found on the left of [cursorPosition] in
+ * the [text]
+ */
+ fun getLastPartOfWord(text: String, word: String, cursorPosition: Int): String {
+ val leadPart = getLeadPartOfWord(text, cursorPosition) ?: return ""
+
+ return word.substring(leadPart.length, word.length)
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt
new file mode 100644
index 0000000000..43ed7ac068
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/SeedPhraseInteractor.kt
@@ -0,0 +1,24 @@
+package com.tangem.feature.onboarding.domain
+
+import com.tangem.crypto.bip39.Mnemonic
+
+/**
+[REDACTED_AUTHOR]
+ */
+interface SeedPhraseInteractor {
+ suspend fun generateMnemonic(): Result
+ suspend fun getMnemonicComponents(): Result>
+ suspend fun isWordMatch(word: String): Boolean
+ suspend fun validateMnemonicString(text: String): Result>
+ suspend fun getSuggestions(text: String, hasSelection: Boolean, cursorPosition: Int): List
+ suspend fun insertSuggestionWord(text: String, suggestion: String, cursorPosition: Int): InsertSuggestionResult
+
+ companion object {
+ const val MNEMONIC_DELIMITER = " "
+ }
+}
+
+data class InsertSuggestionResult(
+ val text: String,
+ val cursorPosition: Int,
+)
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt
new file mode 100644
index 0000000000..108700efe3
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/domain/di/OnboardingDomainModule.kt
@@ -0,0 +1,21 @@
+package com.tangem.feature.onboarding.domain.di
+
+import com.tangem.feature.onboarding.data.MnemonicRepository
+import com.tangem.feature.onboarding.domain.DefaultSeedPhraseInteractor
+import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+class OnboardingDomainModule {
+
+ @Provides
+ @Singleton
+ fun provideSeedPhraseInteractor(repository: MnemonicRepository): SeedPhraseInteractor {
+ return DefaultSeedPhraseInteractor(repository)
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt
new file mode 100644
index 0000000000..842a7a5141
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/ButtonState.kt
@@ -0,0 +1,11 @@
+package com.tangem.feature.onboarding.presentation.wallet2.model
+
+/**
+[REDACTED_AUTHOR]
+ */
+data class ButtonState(
+ val enabled: Boolean = true,
+ val isClickable: Boolean = true,
+ val showProgress: Boolean = false,
+ val onClick: () -> Unit,
+)
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt
new file mode 100644
index 0000000000..db8f0d469b
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/DescriptionResource.kt
@@ -0,0 +1,8 @@
+package com.tangem.feature.onboarding.presentation.wallet2.model
+
+import androidx.annotation.StringRes
+
+data class DescriptionResource(
+ @StringRes val titleRes: Int,
+ @StringRes val subTitleRes: Int,
+)
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt
new file mode 100644
index 0000000000..9eea61c93b
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/OnboardingSeedPhraseState.kt
@@ -0,0 +1,71 @@
+package com.tangem.feature.onboarding.presentation.wallet2.model
+
+import androidx.compose.ui.text.input.TextFieldValue
+import com.tangem.feature.onboarding.domain.SeedPhraseError
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+
+/**
+[REDACTED_AUTHOR]
+ */
+data class OnboardingSeedPhraseState(
+ val step: OnboardingSeedPhraseStep,
+ val introState: IntroState,
+ val aboutState: AboutState,
+ val yourSeedPhraseState: YourSeedPhraseState,
+ val checkSeedPhraseState: CheckSeedPhraseState,
+ val importSeedPhraseState: ImportSeedPhraseState,
+ val menuButtonChat: ButtonState,
+ val isOnboardingFinished: Boolean = false,
+)
+
+enum class OnboardingSeedPhraseStep {
+ Intro, AboutSeedPhrase, YourSeedPhrase, CheckSeedPhrase, ImportSeedPhrase
+}
+
+data class IntroState(
+ val cardImageUrl: String? = null,
+ val buttonCreateWallet: ButtonState,
+ val buttonOtherOptions: ButtonState,
+)
+
+data class AboutState(
+ val buttonReadMoreAboutSeedPhrase: ButtonState,
+ val buttonGenerateSeedPhrase: ButtonState,
+ val buttonImportSeedPhrase: ButtonState,
+)
+
+data class YourSeedPhraseState(
+ val mnemonicGridItems: ImmutableList = persistentListOf(),
+ val buttonContinue: ButtonState,
+)
+
+data class MnemonicGridItem(
+ val index: Int,
+ val mnemonic: String,
+)
+
+data class CheckSeedPhraseState(
+ val tvSecondPhrase: TextFieldState,
+ val tvSeventhPhrase: TextFieldState,
+ val tvEleventhPhrase: TextFieldState,
+ val buttonCreateWallet: ButtonState,
+)
+
+data class ImportSeedPhraseState(
+ val tvSeedPhrase: TextFieldState,
+ val onSuggestedPhraseClick: (Int) -> Unit,
+ val buttonCreateWallet: ButtonState,
+ val invalidWords: Set = emptySet(),
+ val suggestionsList: List = emptyList(),
+ val error: SeedPhraseError? = null,
+)
+
+data class TextFieldState(
+ val onTextFieldValueChanged: (TextFieldValue) -> Unit,
+ val textFieldValue: TextFieldValue = TextFieldValue(),
+ val label: String? = null,
+ val isError: Boolean = false,
+ val isFocused: Boolean = false,
+ val onFocusChanged: (Boolean) -> Unit = {},
+)
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt
new file mode 100644
index 0000000000..419b277b9d
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/model/UiActions.kt
@@ -0,0 +1,56 @@
+package com.tangem.feature.onboarding.presentation.wallet2.model
+
+import androidx.compose.ui.text.input.TextFieldValue
+
+/**
+[REDACTED_AUTHOR]
+ */
+data class UiActions(
+ val introActions: IntroUiAction,
+ val aboutActions: AboutUiAction,
+ val yourSeedPhraseActions: YourSeedPhraseUiAction,
+ val checkSeedPhraseActions: CheckSeedPhraseUiAction,
+ val importSeedPhraseActions: ImportSeedPhraseUiAction,
+ val menuChatClick: () -> Unit,
+)
+
+data class IntroUiAction(
+ val buttonCreateWalletClick: () -> Unit,
+ val buttonOtherOptionsClick: () -> Unit,
+)
+
+data class AboutUiAction(
+ val buttonReadMoreAboutSeedPhraseClick: () -> Unit,
+ val buttonGenerateSeedPhraseClick: () -> Unit,
+ val buttonImportSeedPhraseClick: () -> Unit,
+)
+
+data class YourSeedPhraseUiAction(
+ val buttonContinueClick: () -> Unit,
+)
+
+data class CheckSeedPhraseUiAction(
+ val secondTextFieldAction: TextFieldUiAction,
+ val seventhTextFieldAction: TextFieldUiAction,
+ val eleventhTextFieldAction: TextFieldUiAction,
+ val buttonCreateWalletClick: () -> Unit,
+)
+
+data class ImportSeedPhraseUiAction(
+ val phraseTextFieldAction: TextFieldUiAction,
+ val suggestedPhraseClick: (Int) -> Unit,
+ val buttonCreateWalletClick: () -> Unit,
+)
+
+data class TextFieldUiAction(
+ val onTextFieldChanged: (TextFieldValue) -> Unit,
+ val onFocusChanged: (Boolean) -> Unit = {},
+)
+
+
+
+enum class SeedPhraseField {
+ Second,
+ Seventh,
+ Eleventh,
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt
new file mode 100644
index 0000000000..fc583bccc7
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/AboutSeedPhraseScreen.kt
@@ -0,0 +1,148 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.tangem.core.ui.components.SecondaryButton
+import com.tangem.core.ui.components.SpacerH32
+import com.tangem.core.ui.components.SpacerW8
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.AboutState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun AboutSeedPhraseScreen(state: AboutState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ Box(
+ modifier = Modifier.weight(weight = 0.8f),
+ ) {
+ EditIconBlock()
+ }
+ Box(
+ modifier = Modifier.weight(weight = 1.2f),
+ ) {
+ Column {
+ OnboardingDescriptionBlock {
+ Description(
+ titleRes = R.string.onboarding_seed_intro_title,
+ subTitleRes = R.string.onboarding_seed_intro_message,
+ )
+ }
+ ReadMoreBlock(state)
+ }
+ }
+ Box {
+ OnboardingActionBlock(
+ firstActionContent = {
+ SecondaryButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_seed_intro_button_generate),
+ enabled = state.buttonGenerateSeedPhrase.enabled,
+ showProgress = state.buttonGenerateSeedPhrase.showProgress,
+ onClick = state.buttonGenerateSeedPhrase.onClick,
+ )
+ },
+ secondActionContent = {
+ SecondaryButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_seed_intro_button_import),
+ enabled = state.buttonImportSeedPhrase.enabled,
+ showProgress = state.buttonImportSeedPhrase.showProgress,
+ onClick = state.buttonImportSeedPhrase.onClick,
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun EditIconBlock() {
+ Column(
+ modifier = Modifier
+ .fillMaxSize(),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ Image(
+ modifier = Modifier
+ .size(TangemTheme.dimens.size44),
+ painter = painterResource(id = R.drawable.ic_onboarding_text_edit_56),
+ contentDescription = null,
+ )
+ SpacerH32()
+ Box(
+ modifier = Modifier
+ .background(
+ color = TangemTheme.colors.icon.warning.copy(alpha = 0.12f),
+ shape = RoundedCornerShape(TangemTheme.dimens.radius8),
+ ),
+ ) {
+ Text(
+ modifier = Modifier
+ .padding(
+ horizontal = TangemTheme.dimens.size12,
+ vertical = TangemTheme.dimens.size4,
+ ),
+ text = stringResource(id = R.string.onboarding_seed_phrase_intro_legacy),
+ color = TangemTheme.colors.text.warning,
+ style = TangemTheme.typography.body1,
+ )
+ }
+ }
+}
+
+@Composable
+private fun ReadMoreBlock(state: AboutState) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = TangemTheme.dimens.size16),
+ ) {
+ val shape = TangemTheme.shapes.roundedCornersLarge
+ Row(
+ modifier = Modifier
+ .align(Alignment.Center)
+ .clip(shape)
+ .border(
+ width = TangemTheme.dimens.size1,
+ color = TangemTheme.colors.stroke.primary,
+ shape = shape,
+ )
+ .clickable(onClick = state.buttonReadMoreAboutSeedPhrase.onClick)
+ .padding(
+ horizontal = TangemTheme.dimens.size16,
+ vertical = TangemTheme.dimens.size8,
+ ),
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.ic_arrow_top_right_24),
+ contentDescription = null,
+ )
+ SpacerW8()
+ Text(
+ text = stringResource(id = R.string.onboarding_seed_button_read_more),
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt
new file mode 100644
index 0000000000..b67af3906f
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt
@@ -0,0 +1,98 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import com.tangem.core.ui.R
+import com.tangem.core.ui.components.OutlineTextField
+import com.tangem.core.ui.components.PrimaryButtonIconLeft
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Suppress("ReusedModifierInstance")
+@Composable
+fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier
+ .fillMaxSize(),
+ ) {
+ LazyColumn(
+ modifier = modifier
+ .weight(1f)
+ .fillMaxWidth(),
+ ) {
+
+ item {
+ OnboardingDescriptionBlock(
+ modifier = Modifier
+ .weight(1f)
+ .padding(top = TangemTheme.dimens.size48)
+ .padding(horizontal = TangemTheme.dimens.size16),
+ ) {
+ Description(
+ titleRes = R.string.onboarding_seed_user_validation_title,
+ subTitleRes = R.string.onboarding_seed_user_validation_message,
+ )
+ }
+ }
+ item {
+ CheckSeedPhraseBlock(
+ modifier = Modifier
+ .imePadding()
+ .weight(1f)
+ .padding(top = TangemTheme.dimens.size20)
+ .padding(horizontal = TangemTheme.dimens.size16),
+ state = state,
+ )
+ }
+ }
+ OnboardingActionBlock(
+ firstActionContent = {
+ PrimaryButtonIconLeft(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
+ icon = painterResource(id = R.drawable.ic_tangem_24),
+ enabled = state.buttonCreateWallet.enabled,
+ showProgress = state.buttonCreateWallet.showProgress,
+ onClick = state.buttonCreateWallet.onClick,
+ )
+ },
+ )
+ }
+}
+
+@Composable
+private fun CheckSeedPhraseBlock(state: CheckSeedPhraseState, modifier: Modifier = Modifier) {
+ Column(modifier) {
+ OutlineTextField(
+ value = state.tvSecondPhrase.textFieldValue,
+ onValueChange = state.tvSecondPhrase.onTextFieldValueChanged,
+ label = state.tvSecondPhrase.label,
+ isError = state.tvSecondPhrase.isError,
+ )
+
+ OutlineTextField(
+ value = state.tvSeventhPhrase.textFieldValue,
+ onValueChange = state.tvSeventhPhrase.onTextFieldValueChanged,
+ label = state.tvSeventhPhrase.label,
+ isError = state.tvSeventhPhrase.isError,
+ )
+
+ OutlineTextField(
+ value = state.tvEleventhPhrase.textFieldValue,
+ onValueChange = state.tvEleventhPhrase.onTextFieldValueChanged,
+ label = state.tvEleventhPhrase.label,
+ isError = state.tvEleventhPhrase.isError,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt
new file mode 100644
index 0000000000..1db145ce7f
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt
@@ -0,0 +1,140 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.animation.*
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.material.OutlinedTextField
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.unit.IntOffset
+import com.tangem.core.ui.components.PrimaryButton
+import com.tangem.core.ui.components.PrimaryButtonIconLeft
+import com.tangem.core.ui.components.TangemTextFieldsDefault
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.ImportSeedPhraseState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.DescriptionSubTitleText
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.InvalidWordsColorTransformation
+import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseErrorConverter
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun ImportSeedPhraseScreen(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ Box(
+ modifier = Modifier.weight(1f),
+ ) {
+ Column {
+ OnboardingDescriptionBlock(
+ modifier = Modifier.padding(top = TangemTheme.dimens.size16),
+ ) {
+ DescriptionSubTitleText(text = stringResource(id = R.string.onboarding_seed_import_message))
+ }
+ PhraseBlock(
+ modifier = Modifier
+ .padding(top = TangemTheme.dimens.size62)
+ .padding(horizontal = TangemTheme.dimens.size16),
+ state = state,
+ )
+ SuggestionsBlock(
+ state = state,
+ )
+ }
+ }
+ Box {
+ OnboardingActionBlock(
+ firstActionContent = {
+ PrimaryButtonIconLeft(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
+ icon = painterResource(id = R.drawable.ic_tangem_24),
+ enabled = state.buttonCreateWallet.enabled,
+ showProgress = state.buttonCreateWallet.showProgress,
+ onClick = state.buttonCreateWallet.onClick,
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
+ // TODO: replace by TextReference
+ val errorConverter = remember { SeedPhraseErrorConverter() }
+
+ Column(
+ modifier = modifier.fillMaxWidth(),
+ ) {
+ OutlinedTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(TangemTheme.dimens.size142),
+ value = state.tvSeedPhrase.textFieldValue,
+ onValueChange = state.tvSeedPhrase.onTextFieldValueChanged,
+ textStyle = TangemTheme.typography.body1,
+ singleLine = false,
+ visualTransformation = InvalidWordsColorTransformation(
+ wordsToBrush = state.invalidWords,
+ style = SpanStyle(color = TangemTheme.colors.text.warning),
+ ),
+ colors = TangemTextFieldsDefault.defaultTextFieldColors,
+ )
+
+
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(TangemTheme.dimens.size32),
+ ) {
+ val message = errorConverter.convert(LocalContext.current to state.error)
+ if (message != null) {
+ Text(
+ modifier = Modifier.fillMaxSize(),
+ text = message,
+ style = TangemTheme.typography.caption.copy(
+ color = TangemTheme.colors.text.warning,
+ ),
+ )
+ }
+ }
+ }
+}
+
+@Suppress("ReusedModifierInstance")
+@Composable
+private fun SuggestionsBlock(state: ImportSeedPhraseState, modifier: Modifier = Modifier) {
+ AnimatedVisibility(
+ enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }),
+ exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(),
+ visible = state.suggestionsList.isNotEmpty(),
+ ) {
+ LazyRow(
+ modifier = modifier.fillMaxSize(),
+ contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
+ ) {
+ items(state.suggestionsList.size) { index ->
+ PrimaryButton(
+ modifier = Modifier
+ .height(TangemTheme.dimens.size46)
+ .padding(all = TangemTheme.dimens.size4),
+ text = state.suggestionsList[index],
+ onClick = { state.onSuggestedPhraseClick(index) },
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt
new file mode 100644
index 0000000000..c99a233f33
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/IntroScreen.kt
@@ -0,0 +1,102 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import coil.compose.SubcomposeAsyncImage
+import coil.request.ImageRequest
+import com.tangem.core.ui.components.PrimaryButtonIconLeft
+import com.tangem.core.ui.components.SecondaryButton
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.IntroState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun IntroScreen(state: IntroState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ Box(
+ modifier = Modifier.weight(weight = 1.5f),
+ ) {
+ CardImageBlock(state.cardImageUrl)
+ }
+ Box(
+ modifier = Modifier.weight(weight = 1f),
+ ) {
+ OnboardingDescriptionBlock {
+ Description(
+ titleRes = R.string.onboarding_create_wallet_options_title,
+ subTitleRes = R.string.onboarding_create_wallet_options_message,
+ )
+ }
+ }
+ Box {
+ OnboardingActionBlock(
+ firstActionContent = {
+ PrimaryButtonIconLeft(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_create_wallet_button_create_wallet),
+ icon = painterResource(id = R.drawable.ic_tangem_24),
+ enabled = state.buttonCreateWallet.enabled,
+ showProgress = state.buttonCreateWallet.showProgress,
+ onClick = state.buttonCreateWallet.onClick,
+ )
+ },
+ secondActionContent = {
+ SecondaryButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.onboarding_create_wallet_options_button_options),
+ enabled = state.buttonOtherOptions.enabled,
+ showProgress = state.buttonOtherOptions.showProgress,
+ onClick = state.buttonOtherOptions.onClick,
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun CardImageBlock(cardImageUrl: String? = null) {
+ Box(modifier = Modifier.fillMaxSize()) {
+ val cardImageModifier = Modifier
+ .fillMaxSize()
+ .align(Alignment.Center)
+
+ SubcomposeAsyncImage(
+ modifier = cardImageModifier.padding(horizontal = TangemTheme.dimens.size34),
+ model = ImageRequest.Builder(LocalContext.current)
+ .data(cardImageUrl)
+ .crossfade(true)
+ .build(),
+ loading = { CardPlaceHolder(cardImageModifier) },
+ error = { CardPlaceHolder(cardImageModifier) },
+ contentDescription = null,
+ )
+ }
+}
+
+@Composable
+private fun CardPlaceHolder(modifier: Modifier = Modifier) {
+ Image(
+ modifier = modifier,
+ painter = painterResource(id = R.drawable.card_placeholder_black),
+ contentScale = ContentScale.Fit,
+ contentDescription = null,
+ )
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt
new file mode 100644
index 0000000000..76a7e48923
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/YourSeedPhraseScreen.kt
@@ -0,0 +1,91 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import com.tangem.core.ui.components.PrimaryButton
+import com.tangem.core.ui.components.SpacerW32
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.presentation.wallet2.model.MnemonicGridItem
+import com.tangem.feature.onboarding.presentation.wallet2.model.YourSeedPhraseState
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
+import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun YourSeedPhraseScreen(state: YourSeedPhraseState, modifier: Modifier = Modifier) {
+ Column(
+ modifier = modifier.fillMaxSize(),
+ ) {
+ Box(
+ modifier = Modifier.weight(weight = 0.6f),
+ ) {
+ OnboardingDescriptionBlock(
+ modifier = Modifier.align(Alignment.Center),
+ ) {
+ Description(
+ titleRes = R.string.onboarding_seed_generate_title,
+ subTitleRes = R.string.onboarding_seed_generate_message,
+ )
+ }
+ }
+ Box(
+ modifier = Modifier.weight(weight = 1f),
+ ) {
+ PhraseGreedBlock(state.mnemonicGridItems)
+ }
+
+ Box {
+ OnboardingActionBlock(
+ firstActionContent = {
+ PrimaryButton(
+ modifier = Modifier
+ .fillMaxWidth(),
+ text = stringResource(id = R.string.common_continue),
+ onClick = state.buttonContinue.onClick,
+ )
+ },
+ )
+ }
+ }
+}
+
+@Composable
+private fun PhraseGreedBlock(mnemonicGridItems: ImmutableList) {
+ LazyVerticalGrid(
+ columns = GridCells.Fixed(count = 2),
+ ) {
+ this.items(
+ count = mnemonicGridItems.size,
+ ) { index ->
+ Row(
+ modifier = Modifier.padding(all = TangemTheme.dimens.size8),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ val item = mnemonicGridItems[index]
+ SpacerW32()
+ Text(
+ modifier = Modifier.width(TangemTheme.dimens.size40),
+ text = "${item.index}.",
+ style = TangemTheme.typography.body2,
+ color = TangemTheme.colors.text.secondary,
+ )
+ Text(
+ text = item.mnemonic,
+ style = TangemTheme.typography.button,
+ color = TangemTheme.colors.text.primary1,
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt
new file mode 100644
index 0000000000..b7706cb69e
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingActionBlock.kt
@@ -0,0 +1,38 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.components
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import com.tangem.core.ui.components.SpacerH12
+import com.tangem.core.ui.res.TangemTheme
+
+/**
+[REDACTED_AUTHOR]
+ */
+@Composable
+fun OnboardingActionBlock(
+ firstActionContent: @Composable (() -> Unit)? = null,
+ secondActionContent: @Composable (() -> Unit)? = null,
+) {
+ if (firstActionContent == null && secondActionContent == null) return
+
+ Box(
+ modifier = Modifier
+ .padding(horizontal = TangemTheme.dimens.size16)
+ .padding(
+ top = TangemTheme.dimens.size8,
+ bottom = TangemTheme.dimens.size32,
+ ),
+ ) {
+ Column(
+ modifier = Modifier.align(Alignment.BottomCenter),
+ ) {
+ firstActionContent?.invoke()
+ if (firstActionContent != null && secondActionContent != null) SpacerH12()
+ secondActionContent?.invoke()
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt
new file mode 100644
index 0000000000..27a76c7f38
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/components/OnboardingDescriptionBlock.kt
@@ -0,0 +1,72 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.components
+
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import com.tangem.core.ui.components.SpacerH12
+import com.tangem.core.ui.res.TangemTheme
+import com.tangem.feature.onboarding.presentation.wallet2.model.DescriptionResource
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+ * UI component witch provides single and carousel description for the onboarding process
+ */
+@Composable
+fun OnboardingDescriptionBlock(modifier: Modifier = Modifier, content: @Composable () -> Unit) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(horizontal = TangemTheme.dimens.size24),
+ ) {
+ content()
+ }
+}
+
+@Composable
+fun Description(@StringRes titleRes: Int, @StringRes subTitleRes: Int) {
+ Column {
+ DescriptionTitleText(text = stringResource(id = titleRes))
+ SpacerH12()
+ DescriptionSubTitleText(text = stringResource(id = subTitleRes))
+ }
+}
+
+@Suppress("UnusedPrivateMember")
+@Composable
+fun OnboardingCarouselDescriptionBlock(
+ descriptionsList: ImmutableList,
+ modifier: Modifier = Modifier,
+) {
+ OnboardingDescriptionBlock(modifier) {
+ // TODO: implement
+ }
+}
+
+@Composable
+fun DescriptionTitleText(text: String) {
+ Text(
+ text = text,
+ style = TangemTheme.typography.h2,
+ color = TangemTheme.colors.text.primary1,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
+
+@Composable
+fun DescriptionSubTitleText(text: String) {
+ Text(
+ text = text,
+ style = TangemTheme.typography.subtitle1,
+ color = TangemTheme.colors.text.secondary,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth(),
+ )
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt
new file mode 100644
index 0000000000..aad9659b9d
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/CheckSeedPhraseStateBuilder.kt
@@ -0,0 +1,77 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
+
+import androidx.compose.ui.text.input.TextFieldValue
+import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
+import com.tangem.feature.onboarding.presentation.wallet2.model.SeedPhraseField
+
+/**
+[REDACTED_AUTHOR]
+ */
+class CheckSeedPhraseStateBuilder {
+
+ fun updateTextField(
+ uiState: OnboardingSeedPhraseState,
+ field: SeedPhraseField,
+ textFieldValue: TextFieldValue,
+ ): OnboardingSeedPhraseState = when (field) {
+ SeedPhraseField.Second -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy(
+ textFieldValue = textFieldValue,
+ ),
+ ),
+ )
+ SeedPhraseField.Seventh -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy(
+ textFieldValue = textFieldValue,
+ ),
+ ),
+ )
+ SeedPhraseField.Eleventh -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy(
+ textFieldValue = textFieldValue,
+ ),
+ ),
+ )
+ }
+
+ fun updateTextFieldError(
+ uiState: OnboardingSeedPhraseState,
+ field: SeedPhraseField,
+ hasError: Boolean,
+ ): OnboardingSeedPhraseState = when (field) {
+ SeedPhraseField.Second -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvSecondPhrase = uiState.checkSeedPhraseState.tvSecondPhrase.copy(
+ isError = hasError,
+ ),
+ ),
+ )
+ SeedPhraseField.Seventh -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvSeventhPhrase = uiState.checkSeedPhraseState.tvSeventhPhrase.copy(
+ isError = hasError,
+ ),
+ ),
+ )
+ SeedPhraseField.Eleventh -> uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ tvEleventhPhrase = uiState.checkSeedPhraseState.tvEleventhPhrase.copy(
+ isError = hasError,
+ ),
+ ),
+ )
+ }
+
+ fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState {
+ return uiState.copy(
+ checkSeedPhraseState = uiState.checkSeedPhraseState.copy(
+ buttonCreateWallet = uiState.checkSeedPhraseState.buttonCreateWallet.copy(
+ enabled = enabled,
+ ),
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt
new file mode 100644
index 0000000000..5ffb9b5c41
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/ImportSeedPhraseStateBuilder.kt
@@ -0,0 +1,67 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
+
+import androidx.compose.ui.text.TextRange
+import androidx.compose.ui.text.input.TextFieldValue
+import com.tangem.feature.onboarding.domain.InsertSuggestionResult
+import com.tangem.feature.onboarding.domain.SeedPhraseError
+import com.tangem.feature.onboarding.presentation.wallet2.model.OnboardingSeedPhraseState
+
+/**
+[REDACTED_AUTHOR]
+ */
+class ImportSeedPhraseStateBuilder {
+
+ fun updateTextField(
+ uiState: OnboardingSeedPhraseState,
+ textFieldValue: TextFieldValue,
+ ): OnboardingSeedPhraseState = uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ tvSeedPhrase = uiState.importSeedPhraseState.tvSeedPhrase.copy(
+ textFieldValue = textFieldValue,
+ ),
+ ),
+ )
+
+ fun updateCreateWalletButton(uiState: OnboardingSeedPhraseState, enabled: Boolean): OnboardingSeedPhraseState {
+ return uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ buttonCreateWallet = uiState.importSeedPhraseState.buttonCreateWallet.copy(
+ enabled = enabled,
+ ),
+ ),
+ )
+ }
+
+ fun updateInvalidWords(uiState: OnboardingSeedPhraseState, invalidWords: Set): OnboardingSeedPhraseState =
+ uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ invalidWords = invalidWords,
+ ),
+ )
+
+ fun updateSuggestions(uiState: OnboardingSeedPhraseState, suggestions: List): OnboardingSeedPhraseState =
+ uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ suggestionsList = suggestions,
+ ),
+ )
+
+ fun insertSuggestionWord(
+ uiState: OnboardingSeedPhraseState,
+ insertResult: InsertSuggestionResult,
+ ): OnboardingSeedPhraseState {
+ val newTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue.copy(
+ text = insertResult.text,
+ selection = TextRange(insertResult.cursorPosition, insertResult.cursorPosition),
+ )
+ return updateTextField(uiState, newTextFieldValue)
+ }
+
+ fun updateError(uiState: OnboardingSeedPhraseState, error: SeedPhraseError?): OnboardingSeedPhraseState {
+ return uiState.copy(
+ importSeedPhraseState = uiState.importSeedPhraseState.copy(
+ error = error,
+ ),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt
new file mode 100644
index 0000000000..f240c101ce
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/stateBuiders/StateBuilder.kt
@@ -0,0 +1,126 @@
+package com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders
+
+import com.tangem.feature.onboarding.presentation.wallet2.model.*
+import kotlinx.collections.immutable.ImmutableList
+
+/**
+[REDACTED_AUTHOR]
+ */
+class StateBuilder(
+ private val uiActions: UiActions,
+) {
+
+ val checkSeedPhrase: CheckSeedPhraseStateBuilder = CheckSeedPhraseStateBuilder()
+ val importSeedPhrase: ImportSeedPhraseStateBuilder = ImportSeedPhraseStateBuilder()
+
+ fun init(): OnboardingSeedPhraseState = OnboardingSeedPhraseState(
+ step = OnboardingSeedPhraseStep.Intro,
+ introState = IntroState(
+ buttonCreateWallet = ButtonState(
+ onClick = uiActions.introActions.buttonCreateWalletClick,
+ ),
+ buttonOtherOptions = ButtonState(
+ onClick = uiActions.introActions.buttonOtherOptionsClick,
+ ),
+ ),
+ aboutState = AboutState(
+ buttonReadMoreAboutSeedPhrase = ButtonState(
+ onClick = uiActions.aboutActions.buttonReadMoreAboutSeedPhraseClick,
+ ),
+ buttonGenerateSeedPhrase = ButtonState(
+ onClick = uiActions.aboutActions.buttonGenerateSeedPhraseClick,
+ ),
+ buttonImportSeedPhrase = ButtonState(
+ onClick = uiActions.aboutActions.buttonImportSeedPhraseClick,
+ ),
+ ),
+ yourSeedPhraseState = YourSeedPhraseState(
+ buttonContinue = ButtonState(
+ onClick = uiActions.yourSeedPhraseActions.buttonContinueClick,
+ ),
+ ),
+ checkSeedPhraseState = CheckSeedPhraseState(
+ tvSecondPhrase = TextFieldState(
+ label = "2",
+ isFocused = true,
+ onTextFieldValueChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onTextFieldChanged,
+ onFocusChanged = uiActions.checkSeedPhraseActions.secondTextFieldAction.onFocusChanged,
+ ),
+ tvSeventhPhrase = TextFieldState(
+ label = "7",
+ isFocused = false,
+ onTextFieldValueChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onTextFieldChanged,
+ onFocusChanged = uiActions.checkSeedPhraseActions.seventhTextFieldAction.onFocusChanged,
+ ),
+ tvEleventhPhrase = TextFieldState(
+ label = "11",
+ isFocused = false,
+ onTextFieldValueChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onTextFieldChanged,
+ onFocusChanged = uiActions.checkSeedPhraseActions.eleventhTextFieldAction.onFocusChanged,
+ ),
+ buttonCreateWallet = ButtonState(
+ enabled = false,
+ onClick = uiActions.checkSeedPhraseActions.buttonCreateWalletClick,
+ ),
+ ),
+ importSeedPhraseState = ImportSeedPhraseState(
+ tvSeedPhrase = TextFieldState(
+ onTextFieldValueChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onTextFieldChanged,
+ onFocusChanged = uiActions.importSeedPhraseActions.phraseTextFieldAction.onFocusChanged,
+ ),
+ onSuggestedPhraseClick = uiActions.importSeedPhraseActions.suggestedPhraseClick,
+ buttonCreateWallet = ButtonState(
+ enabled = false,
+ onClick = uiActions.importSeedPhraseActions.buttonCreateWalletClick,
+ ),
+ ),
+ menuButtonChat = ButtonState(
+ onClick = uiActions.menuChatClick,
+ ),
+ )
+
+ fun changeStep(uiState: OnboardingSeedPhraseState, step: OnboardingSeedPhraseStep): OnboardingSeedPhraseState =
+ uiState.copy(
+ step = step,
+ )
+
+ fun generateMnemonicComponents(uiState: OnboardingSeedPhraseState): OnboardingSeedPhraseState {
+ return uiState.copy(
+ aboutState = uiState.aboutState.copy(
+ buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy(
+ showProgress = true,
+ ),
+ buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy(
+ enabled = false,
+ ),
+ ),
+ )
+ }
+
+ fun mnemonicGenerated(
+ uiState: OnboardingSeedPhraseState,
+ mnemonicGridItems: ImmutableList,
+ ): OnboardingSeedPhraseState {
+ return updateMnemonicComponents(uiState, mnemonicGridItems)
+ .copy(
+ step = OnboardingSeedPhraseStep.YourSeedPhrase,
+ aboutState = uiState.aboutState.copy(
+ buttonGenerateSeedPhrase = uiState.aboutState.buttonGenerateSeedPhrase.copy(
+ showProgress = false,
+ ),
+ buttonImportSeedPhrase = uiState.aboutState.buttonImportSeedPhrase.copy(
+ enabled = true,
+ ),
+ ),
+ )
+ }
+
+ fun updateMnemonicComponents(
+ uiState: OnboardingSeedPhraseState,
+ mnemonicGridItems: ImmutableList,
+ ): OnboardingSeedPhraseState = uiState.copy(
+ yourSeedPhraseState = uiState.yourSeedPhraseState.copy(
+ mnemonicGridItems = mnemonicGridItems,
+ ),
+ )
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt
new file mode 100644
index 0000000000..7d6f3a4685
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/InvalidWordsColorTransformation.kt
@@ -0,0 +1,79 @@
+package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
+
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.text.SpanStyle
+import androidx.compose.ui.text.buildAnnotatedString
+import androidx.compose.ui.text.input.OffsetMapping
+import androidx.compose.ui.text.input.TransformedText
+import androidx.compose.ui.text.input.VisualTransformation
+import com.tangem.utils.extensions.isSingleItem
+
+/**
+[REDACTED_AUTHOR]
+ */
+class InvalidWordsColorTransformation(
+ private val wordsToBrush: Set,
+ private val style: SpanStyle,
+) : VisualTransformation {
+
+ override fun filter(text: AnnotatedString): TransformedText {
+ val plainText = text.text
+ if (wordsToBrush.isSingleItem() && wordsToBrush.first() == plainText) {
+ return plainText.annotate().toTransformedText()
+ }
+
+ val wordList = makeSeparatedWordlist(plainText)
+ val annotatedString = makeAnnotatedString(wordList)
+
+ return annotatedString.toTransformedText()
+ }
+
+ private fun makeSeparatedWordlist(plainText: String): List {
+ val plainTextCharArray = plainText.toCharArray()
+ val wordlist = mutableListOf()
+ var wordBuilder = StringBuilder()
+
+ for (index in plainTextCharArray.indices) {
+ val char = plainTextCharArray[index]
+ if (char.isWhitespace()) {
+ if (wordBuilder.isEmpty()) {
+ wordlist.add(WORD_SEPARATOR)
+ } else {
+ wordlist.add(wordBuilder.toString())
+ wordBuilder = StringBuilder()
+ wordlist.add(WORD_SEPARATOR)
+ }
+ } else {
+ if (index == plainTextCharArray.lastIndex) {
+ wordBuilder.append(char)
+ wordlist.add(wordBuilder.toString())
+ wordBuilder = StringBuilder()
+ } else {
+ wordBuilder.append(char)
+ }
+ }
+ }
+ return wordlist.toList()
+ }
+
+ private fun makeAnnotatedString(wordList: List): AnnotatedString = buildAnnotatedString {
+ wordList.forEach {
+ if (it == WORD_SEPARATOR) {
+ append(WHITE_SPACE)
+ } else if (wordsToBrush.contains(it)) {
+ append(it.annotate())
+ } else {
+ append(it)
+ }
+ }
+ }
+
+ private fun String.annotate(): AnnotatedString = AnnotatedString(this, style)
+
+ private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity)
+
+ companion object {
+ private const val WHITE_SPACE = " "
+ private const val WORD_SEPARATOR = "_$@-*"
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt
new file mode 100644
index 0000000000..dbb553ba63
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseErrorConverter.kt
@@ -0,0 +1,28 @@
+package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
+
+import android.content.Context
+import com.tangem.common.module.ModuleMessageConverter
+import com.tangem.feature.onboarding.R
+import com.tangem.feature.onboarding.domain.SeedPhraseError
+
+/**
+[REDACTED_AUTHOR]
+ */
+class SeedPhraseErrorConverter : ModuleMessageConverter, String?> {
+
+ override fun convert(source: Pair): String? {
+ val (context, message) = source
+
+ val convertedMessage = when (message) {
+ SeedPhraseError.InvalidEntropyLength -> {
+ context.getString(R.string.onboarding_seed_mnemonic_invalid_checksum)
+ }
+ is SeedPhraseError.InvalidWords -> {
+ context.getString(R.string.onboarding_seed_mnemonic_wrong_words)
+ }
+ else -> null
+ }
+
+ return convertedMessage
+ }
+}
\ No newline at end of file
diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt
new file mode 100644
index 0000000000..4fbee7352f
--- /dev/null
+++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/viewmodel/SeedPhraseViewModel.kt
@@ -0,0 +1,318 @@
+package com.tangem.feature.onboarding.presentation.wallet2.viewmodel
+
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.tangem.feature.onboarding.domain.SeedPhraseError
+import com.tangem.feature.onboarding.domain.SeedPhraseInteractor
+import com.tangem.feature.onboarding.presentation.wallet2.model.*
+import com.tangem.feature.onboarding.presentation.wallet2.ui.stateBuiders.StateBuilder
+import com.tangem.utils.coroutines.CoroutineDispatcherProvider
+import com.tangem.utils.coroutines.Debouncer
+import com.tangem.utils.extensions.isEven
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.*
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.inject.Inject
+
+/**
+[REDACTED_AUTHOR]
+ */
+@HiltViewModel
+class SeedPhraseViewModel @Inject constructor(
+ private val interactor: SeedPhraseInteractor,
+ private val dispatchers: CoroutineDispatcherProvider,
+) : ViewModel() {
+
+ private var uiBuilder = StateBuilder(createUiActions())
+
+ var uiState: OnboardingSeedPhraseState by mutableStateOf(uiBuilder.init())
+ private set
+
+ val currentStep: OnboardingSeedPhraseStep
+ get() = uiState.step
+
+ private val textFieldsDebouncers = mutableMapOf()
+ private var suggestionWordInserted: AtomicBoolean = AtomicBoolean(false)
+
+ override fun onCleared() {
+ textFieldsDebouncers.forEach { entry -> entry.value.release() }
+ textFieldsDebouncers.clear()
+ super.onCleared()
+ }
+
+ private fun createUiActions(): UiActions = UiActions(
+ introActions = IntroUiAction(
+ buttonCreateWalletClick = ::buttonCreateWalletClick,
+ buttonOtherOptionsClick = ::buttonOtherOptionsClick,
+ ),
+ aboutActions = AboutUiAction(
+ buttonReadMoreAboutSeedPhraseClick = ::buttonReadMoreAboutSeedPhraseClick,
+ buttonGenerateSeedPhraseClick = ::buttonGenerateSeedPhraseClick,
+ buttonImportSeedPhraseClick = ::buttonImportSeedPhraseClick,
+ ),
+ yourSeedPhraseActions = YourSeedPhraseUiAction(
+ buttonContinueClick = ::buttonContinueClick,
+ ),
+ checkSeedPhraseActions = CheckSeedPhraseUiAction(
+ buttonCreateWalletClick = ::buttonCreateWalletWithSeedPhraseClick,
+ secondTextFieldAction = TextFieldUiAction(
+ onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Second, value) },
+ ),
+ seventhTextFieldAction = TextFieldUiAction(
+ onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Seventh, value) },
+ ),
+ eleventhTextFieldAction = TextFieldUiAction(
+ onTextFieldChanged = { value -> onTextFieldChanged(SeedPhraseField.Eleventh, value) },
+ ),
+ ),
+ importSeedPhraseActions = ImportSeedPhraseUiAction(
+ phraseTextFieldAction = TextFieldUiAction(
+ onTextFieldChanged = { value -> onSeedPhraseTextFieldChanged(value) },
+ ),
+ suggestedPhraseClick = ::buttonSuggestedPhraseClick,
+ buttonCreateWalletClick = ::buttonCreateWalletWithSeedPhraseClick,
+ ),
+ menuChatClick = ::menuChatClick,
+ )
+
+ // region CheckSeedPhrase
+ private fun onTextFieldChanged(field: SeedPhraseField, textFieldValue: TextFieldValue) {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.checkSeedPhrase.updateTextField(uiState, field, textFieldValue) }
+
+ val fieldState = field.getState(uiState)
+ if (fieldState.textFieldValue.text.isEmpty()) {
+ updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError = false) }
+ return@launchSingle
+ }
+
+ createOrGetDebouncer(field.name).debounce(viewModelScope, context = dispatchers.io) {
+ val hasError = !interactor.isWordMatch(textFieldValue.text)
+ if (fieldState.isError != hasError) {
+ updateUi { uiBuilder.checkSeedPhrase.updateTextFieldError(uiState, field, hasError) }
+ }
+
+ val allFieldsWithoutError = SeedPhraseField.values()
+ .map { field -> field.getState(uiState) }
+ .all { fieldState -> !fieldState.isError }
+
+ if (uiState.checkSeedPhraseState.buttonCreateWallet.enabled != allFieldsWithoutError) {
+ updateUi { uiBuilder.checkSeedPhrase.updateCreateWalletButton(uiState, allFieldsWithoutError) }
+ }
+ }
+ }
+ }
+ // endregion CheckSeedPhrase
+
+ // region ImportSeedPhrase
+ private fun onSeedPhraseTextFieldChanged(textFieldValue: TextFieldValue) {
+ val oldTextFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue
+ val isSameText = textFieldValue.text == oldTextFieldValue.text
+ val isCursorMoved = textFieldValue.selection != oldTextFieldValue.selection
+
+ val mediateState = uiBuilder.importSeedPhrase.updateTextField(uiState, textFieldValue)
+ uiState = uiBuilder.importSeedPhrase.updateCreateWalletButton(mediateState, enabled = false)
+
+ val fieldState = uiState.importSeedPhraseState.tvSeedPhrase
+ val inputMnemonic = fieldState.textFieldValue.text
+
+ val debouncer = createOrGetDebouncer(MNEMONIC_DEBOUNCER)
+ when {
+ suggestionWordInserted.getAndSet(false) -> {
+ debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
+ validateMnemonic(inputMnemonic)
+ }
+ }
+ isSameText && !isCursorMoved -> {
+ // do nothing
+ }
+ isSameText && isCursorMoved -> {
+ debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
+ updateSuggestions(fieldState)
+ }
+ }
+ else -> {
+ debouncer.debounce(viewModelScope, MNEMONIC_DEBOUNCE_DELAY, dispatchers.single) {
+ updateSuggestions(fieldState)
+ validateMnemonic(inputMnemonic)
+ }
+ }
+ }
+ }
+
+ private suspend fun validateMnemonic(inputMnemonic: String) {
+ if (inputMnemonic.isEmpty() && uiState.importSeedPhraseState.error != null) {
+ updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) }
+ return
+ }
+
+ interactor.validateMnemonicString(inputMnemonic)
+ .onSuccess {
+ updateUi { uiBuilder.importSeedPhrase.updateError(uiState, null) }
+ }
+ .onFailure {
+ val error = it as? SeedPhraseError ?: return
+
+ val mediateState = when (error) {
+ is SeedPhraseError.InvalidWords -> {
+ uiBuilder.importSeedPhrase.updateInvalidWords(uiState, error.words)
+ }
+ else -> uiState
+ }
+ updateUi { uiBuilder.importSeedPhrase.updateError(mediateState, error) }
+ }
+ }
+
+ private suspend fun updateSuggestions(fieldState: TextFieldState) {
+ val suggestions = interactor.getSuggestions(
+ text = fieldState.textFieldValue.text,
+ hasSelection = !fieldState.textFieldValue.selection.collapsed,
+ cursorPosition = fieldState.textFieldValue.selection.end,
+ )
+ updateUi { uiBuilder.importSeedPhrase.updateSuggestions(uiState, suggestions) }
+ }
+ // endregion ImportSeedPhrase
+
+ // region ButtonClickHandlers
+ @Suppress("EmptyFunctionBlock")
+ private fun buttonCreateWalletClick() {
+ }
+
+ @Suppress("EmptyFunctionBlock")
+ private fun buttonCreateWalletWithSeedPhraseClick() {
+ }
+
+ private fun buttonOtherOptionsClick() {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.AboutSeedPhrase) }
+ }
+ }
+
+ private fun buttonReadMoreAboutSeedPhraseClick() {
+ // TODO: open the about info through a router
+ }
+
+ private fun buttonGenerateSeedPhraseClick() {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.generateMnemonicComponents(uiState) }
+ delay(DELAY_GENERATE_SEED_PHRASE)
+ interactor.generateMnemonic()
+ .onSuccess { mnemonic ->
+ val mnemonicGridItems = generateMnemonicGridList(mnemonic.mnemonicComponents)
+ updateUi { uiBuilder.mnemonicGenerated(uiState, mnemonicGridItems.toImmutableList()) }
+ }
+ .onFailure {
+ // TODO: show error
+ }
+ }
+ }
+
+ private suspend fun generateMnemonicGridList(mnemonicComponents: List): ImmutableList {
+ val size = mnemonicComponents.size
+ val splitIndex = if (size.isEven()) size / 2 else size / 2 + 1
+ val leftColumn = mnemonicComponents.subList(0, splitIndex)
+ val rightColumn = mnemonicComponents.subList(splitIndex, size)
+
+ val mnemonicGridItems = mutableListOf()
+ for (index in 0 until splitIndex) {
+ if (index <= leftColumn.size) {
+ val item = MnemonicGridItem(
+ index = index + 1,
+ mnemonic = leftColumn[index],
+ )
+ mnemonicGridItems.add(item)
+ }
+ if (index < rightColumn.size) {
+ val item = MnemonicGridItem(
+ index = index + splitIndex + 1,
+ mnemonic = rightColumn[index],
+ )
+ mnemonicGridItems.add(item)
+ }
+ }
+ return mnemonicGridItems.toImmutableList()
+ }
+
+ private fun buttonImportSeedPhraseClick() {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.ImportSeedPhrase) }
+ }
+ }
+
+ private fun buttonContinueClick() {
+ viewModelScope.launchSingle {
+ updateUi { uiBuilder.changeStep(uiState, OnboardingSeedPhraseStep.CheckSeedPhrase) }
+ }
+ }
+
+ private fun buttonSuggestedPhraseClick(suggestionIndex: Int) {
+ viewModelScope.launchSingle {
+ suggestionWordInserted.set(true)
+ val textFieldValue = uiState.importSeedPhraseState.tvSeedPhrase.textFieldValue
+ val word = uiState.importSeedPhraseState.suggestionsList[suggestionIndex]
+ val cursorPosition = textFieldValue.selection.end
+
+ val insertResult = interactor.insertSuggestionWord(
+ text = textFieldValue.text,
+ suggestion = word,
+ cursorPosition = cursorPosition,
+ )
+ updateUi {
+ val mediateState = uiBuilder.importSeedPhrase.insertSuggestionWord(uiState, insertResult)
+ uiBuilder.importSeedPhrase.updateSuggestions(mediateState, emptyList())
+ }
+ }
+ }
+
+ private fun menuChatClick() {
+ // TODO: open the support chat through a router
+ }
+ // endregion ButtonClickHandlers
+
+ // region Utils
+ /**
+ * Updating the UI with a contract where all copying of objects are called in the Single thread context and
+ * updating the UiState in the main context.
+ */
+ private suspend fun updateUi(updateBlock: suspend () -> OnboardingSeedPhraseState) {
+ withSingleContext {
+ val newState = updateBlock.invoke()
+ withMainContext { uiState = newState }
+ }
+ }
+
+ private fun createOrGetDebouncer(name: String): Debouncer {
+ return textFieldsDebouncers[name] ?: Debouncer().apply { textFieldsDebouncers[name] = this }
+ }
+
+ private fun SeedPhraseField.getState(uiState: OnboardingSeedPhraseState): TextFieldState = when (this) {
+ SeedPhraseField.Second -> uiState.checkSeedPhraseState.tvSecondPhrase
+ SeedPhraseField.Seventh -> uiState.checkSeedPhraseState.tvSeventhPhrase
+ SeedPhraseField.Eleventh -> uiState.checkSeedPhraseState.tvEleventhPhrase
+ }
+
+ private fun CoroutineScope.launchSingle(block: suspend CoroutineScope.() -> Unit): Job {
+ return viewModelScope.launch(dispatchers.single, block = block)
+ }
+
+ private suspend fun withMainContext(block: suspend CoroutineScope.() -> T): T {
+ return withContext(dispatchers.main, block)
+ }
+
+ private suspend fun withSingleContext(block: suspend CoroutineScope.() -> T): T {
+ return withContext(dispatchers.single, block)
+ }
+ // endregion Utils
+
+ companion object {
+ private const val MNEMONIC_DEBOUNCER = "MnemonicDebouncer"
+ private const val MNEMONIC_DEBOUNCE_DELAY = 700L
+ private const val DELAY_GENERATE_SEED_PHRASE = 300L
+ }
+}
\ No newline at end of file
diff --git a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
index 1c1ea1538f..ffd683f981 100644
--- a/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
+++ b/features/swap/presentation/src/main/java/com/tangem/feature/swap/viewmodels/SwapViewModel.kt
@@ -359,7 +359,7 @@ internal class SwapViewModel @Inject constructor(
lastAmount.value = cutValue
uiState =
stateBuilder.updateSwapAmount(uiState, inputNumberFormatter.formatWithThousands(cutValue, decimals))
- amountDebouncer.debounce(DEBOUNCE_AMOUNT_DELAY, viewModelScope) {
+ amountDebouncer.debounce(viewModelScope, DEBOUNCE_AMOUNT_DELAY) {
startLoadingQuotes(fromToken, toToken, lastAmount.value)
}
}
diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt
index c64548832a..7ce0c9ff6d 100644
--- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt
+++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/ui/FeatureTogglesScreen.kt
@@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemTheme
@@ -62,9 +63,11 @@ private fun FeatureToggleItem(toggle: TesterFeatureToggle, onCheckedChange: (Boo
) {
Text(
text = toggle.name,
+ modifier = Modifier.weight(1f),
color = TangemTheme.colors.text.primary1,
+ overflow = TextOverflow.Ellipsis,
maxLines = 1,
- style = TangemTheme.typography.body1,
+ style = TangemTheme.typography.body2,
)
Switch(
checked = toggle.isEnabled,
diff --git a/gradle.properties b/gradle.properties
index e3a622a5ef..6f02666fb4 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -6,7 +6,7 @@
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
-org.gradle.jvmargs = -Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+org.gradle.jvmargs = -Xmx4096m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml
index 74754dcdc8..47877b952c 100644
--- a/gradle/dependencies.toml
+++ b/gradle/dependencies.toml
@@ -24,7 +24,7 @@ androidx-paging = "3.1.1"
compose-compiler = "1.4.3"
compose-runtime = "1.3.3"
compose-foundation = "1.3.1"
-compose-material = "1.3.1"
+compose-material = "1.4.2"
compose-constraint = "1.0.1"
compose-navigation = "2.5.3"
compose-accompanist = "0.28.0"
@@ -65,16 +65,17 @@ xmlShimmer = "1.1.3"
zendeskChat = "3.3.5"
zendeskMessaging = "5.2.4"
zxingQrBarcodeScanner = "1.9.8"
-zxingQrCode = "3.3.3"
+zxingQrCode = "3.5.1"
mviCore = "1.3.1"
kotlinSerialization = "1.4.1"
+arrow = "1.2.0-RC"
# endregion Other libraries
# region Tangem
-tangemBlockchainSdk = "develop-205-fix-sending-205"
+tangemBlockchainSdk = "develop-209"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
-tangemCardSdk = "develop-221"
-# tangemCardSdk = "0.0.1" # Keep it! - used for local builds
+tangemCardSdk = "develop-229"
+#tangemCardSdk = "0.0.1" # Keep it! - used for local builds
# endregion Tangem
# region Tools
@@ -206,4 +207,6 @@ zxing-qrBarcodeScanner = { module = "me.dm7.barcodescanner:zxing", version.ref =
zxing-qrCore = { module = "com.google.zxing:core", version.ref = "zxingQrCode" }
mviCore-watcher = { module = "com.github.badoo.mvicore:mvicore-diff", version.ref = "mviCore" }
kotlin-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinSerialization" }
+arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrow" }
+arrow-fx = { module = "io.arrow-kt:arrow-fx-coroutines", version.ref = "arrow" }
# endregion Other
diff --git a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt
index 5d0b3a45e4..c1af6d55fe 100644
--- a/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt
+++ b/plugins/configuration/src/main/kotlin/com/tangem/plugin/configuration/configurations/extension/BaseExtensionConfigurations.kt
@@ -20,6 +20,7 @@ internal fun BaseExtension.configureCompilerOptions() {
internal fun BaseExtension.configureCompose(project: Project) {
val useCompose = with(project.path) {
contains(":ui") ||
+ contains(":onboarding") ||
contains(":presentation") ||
contains(":app") || // TODO: [REDACTED_JIRA]
contains(":tester:impl") // TODO: Rename module
diff --git a/settings.gradle.kts b/settings.gradle.kts
index dbb3202511..70f5aeb9e2 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -29,7 +29,6 @@ dependencyResolutionManagement {
}
include(":app")
-include(":domain")
include(":common")
// region Core modules
@@ -47,6 +46,7 @@ include(":libs:auth")
// endregion Libs modules
// region Feature modules
+include(":features:onboarding")
include(":features:referral:data")
include(":features:referral:domain")
include(":features:referral:presentation")
@@ -57,4 +57,13 @@ include(":features:swap:presentation")
include(":features:tester:api")
include(":features:tester:impl")
-// endregion Feature modules
\ No newline at end of file
+// endregion Feature modules
+
+// region Domain modules
+// TODO: Remove, temporary modules
+include(":domain:models")
+include(":domain:legacy")
+
+include(":domain:core")
+include(":domain:card")
+// endregion Domain modules
\ No newline at end of file