Updated on 2026-08-14
This commit is contained in:
commit
1b48298775
284 changed files with 8290 additions and 1864 deletions
4
.idea/codeStyles/Project.xml
generated
4
.idea/codeStyles/Project.xml
generated
|
|
@ -9,8 +9,6 @@
|
|||
<package name="io.ktor" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="ALLOW_TRAILING_COMMA" value="true" />
|
||||
<option name="BLANK_LINES_BEFORE_DECLARATION_WITH_COMMENT_OR_ANNOTATION_ON_SEPARATE_LINE" value="0" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
|
|
@ -200,4 +198,4 @@
|
|||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
</component>
|
||||
5
.idea/inspectionProfiles/Project_Default.xml
generated
5
.idea/inspectionProfiles/Project_Default.xml
generated
|
|
@ -15,6 +15,9 @@
|
|||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
|
|
@ -85,8 +88,6 @@
|
|||
<inspection_tool class="SimplifyWhenWithBooleanConstantCondition" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryOptInAnnotation" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
<inspection_tool class="UnnecessaryVariable" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnusedReceiverParameter" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="UnusedSymbol" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
<inspection_tool class="WhenWithOnlyElse" enabled="true" level="ERROR" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -473,6 +473,17 @@
|
|||
"networkId": "kava/test"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "ravencoin",
|
||||
"symbol": "RVN",
|
||||
"name": "Ravencoin",
|
||||
"networks":
|
||||
[
|
||||
{
|
||||
"networkId": "ravencoin/test"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -35,12 +35,40 @@ sealed class ManageTokens(
|
|||
params: Map<String, String> = 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<String, String> = 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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<SuccessResponse> {
|
||||
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 <T : CommandResponse> runTaskAsync(
|
||||
suspend fun <T> runTaskAsync(
|
||||
runnable: CardSessionRunnable<T>,
|
||||
cardId: String? = null,
|
||||
initialMessage: Message? = null,
|
||||
|
|
@ -198,7 +207,7 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun <T : CommandResponse> runTaskAsyncReturnOnMain(
|
||||
private suspend fun <T> runTaskAsyncReturnOnMain(
|
||||
runnable: CardSessionRunnable<T>,
|
||||
cardId: String? = null,
|
||||
initialMessage: Message? = null,
|
||||
|
|
|
|||
|
|
@ -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<List<ByteArray>> {
|
||||
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 ->
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<Blockchain> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<List<UserWalletEncryptionKey>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<String, Any>): DerivationPath {
|
||||
val rawPath = jsonMap["rawPath"] as String
|
||||
val nodeIndexes = (jsonMap["nodes"] as? List<Number>).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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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\"}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<CoinsResponse.Coin, FoundToken> {
|
||||
|
||||
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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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<AddCustomTokenViewModel>().apply {
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(this)
|
||||
}
|
||||
|
||||
TangemTheme {
|
||||
AddCustomTokenScreen(stateHolder = viewModel.uiState)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TokensCategoryBlock>,
|
||||
val onTestTokenClick: (String) -> Unit,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Tokens category model
|
||||
*
|
||||
* @property name category name
|
||||
* @property items category items
|
||||
*/
|
||||
data class TokensCategoryBlock(val name: String, val items: List<TestTokenItem>)
|
||||
|
||||
/**
|
||||
* 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<SelectorItem>
|
||||
|
||||
/** 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<SelectorItem.Title>,
|
||||
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<SelectorItem.TitleWithSubtitle>,
|
||||
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)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TextReference>
|
||||
|
||||
/** 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<TextReference> = 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<TextReference>,
|
||||
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<TextReference>,
|
||||
override val floatingButton: AddCustomTokenFloatingButton,
|
||||
val testBlock: AddCustomTokenTestBlock,
|
||||
val bottomSheet: AddCustomTokenChooseTokenBottomSheet,
|
||||
) : AddCustomTokenStateHolder
|
||||
}
|
||||
|
|
@ -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 = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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<TestTokenItem>, 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 = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {}))
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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 = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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<AddCustomTokenSelectorField.SelectorItem.Title> {
|
||||
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<AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle> {
|
||||
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"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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<AppState> = { _, 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<CardInfo>? = 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(),
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
|
|||
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<AppState>) {
|
|||
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<AppState>) {
|
|||
Analytics.send(Settings.CardSettings.ButtonChangeSecurityMode())
|
||||
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
|
||||
}
|
||||
is CardInfo.AccessCodeRecovery -> {
|
||||
store.dispatch(DetailsAction.AccessCodeRecovery.Open)
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue