Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-29 15:46:45 +03:00
commit 6c6112283c
433 changed files with 6045 additions and 6454 deletions

View file

@ -1,13 +0,0 @@
[*.{kt,kts}]
indent_size = 4
indent_style = space
insert_final_newline = true
ij_kotlin_allow_trailing_comma = true
ij_kotlin_allow_trailing_comma_on_call_site = true
ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^
ij_kotlin_packages_to_use_import_on_demand = java.util.*, kotlinx.android.synthetic.**
max_line_length = 120

View file

@ -2,6 +2,10 @@
<code_scheme name="Project" version="173">
<option name="RIGHT_MARGIN" value="120" />
<option name="SOFT_MARGINS" value="120" />
<JavaCodeStyleSettings>
<option name="CLASS_COUNT_TO_USE_IMPORT_ON_DEMAND" value="5" />
<option name="NAMES_COUNT_TO_USE_IMPORT_ON_DEMAND" value="3" />
</JavaCodeStyleSettings>
<JetCodeStyleSettings>
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
<value>
@ -9,6 +13,9 @@
<package name="io.ktor" alias="false" withSubpackages="true" />
</value>
</option>
<option name="LINE_BREAK_AFTER_MULTILINE_WHEN_ENTRY" value="false" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="5" />
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="3" />
<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" />
@ -17,6 +24,9 @@
<option name="SPACES_AROUND_KEY_VALUE_DELIMITER" value="true" />
<option name="KEEP_BLANK_LINES" value="true" />
</Properties>
<editorconfig>
<option name="ENABLED" value="false" />
</editorconfig>
<codeStyleSettings language="CMake">
<indentOptions>
<option name="CONTINUATION_INDENT_SIZE" value="4" />
@ -176,9 +186,6 @@
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
<option name="EXTENDS_LIST_WRAP" value="5" />
<option name="METHOD_CALL_CHAIN_WRAP" value="5" />
<option name="ASSIGNMENT_WRAP" value="5" />
<option name="PARAMETER_ANNOTATION_WRAP" value="5" />
<option name="VARIABLE_ANNOTATION_WRAP" value="5" />
<option name="ENUM_CONSTANTS_WRAP" value="5" />

View file

@ -17,6 +17,7 @@
</inspection_tool>
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />

View file

@ -13,6 +13,8 @@ dependencies {
implementation(files("libs/walletconnect-1.5.6.aar"))
implementation(project(":domain:legacy"))
implementation(project(":domain:models"))
implementation(project(":domain:core"))
implementation(project(":domain:card"))
implementation(project(":common"))
implementation(project(":core:analytics"))
implementation(project(":core:featuretoggles"))
@ -22,6 +24,7 @@ dependencies {
implementation(project(":core:utils"))
implementation(project(":libs:crypto"))
implementation(project(":libs:auth"))
implementation(project(":data:source:preferences"))
/** Features */
implementation(project(":features:onboarding"))

View file

@ -484,6 +484,17 @@
"networkId": "ravencoin/test"
}
]
},
{
"id": "cosmos",
"symbol": "ATOM",
"name": "Cosmos Hub",
"networks":
[
{
"networkId": "cosmos/test"
}
]
}
]
}

View file

@ -12,6 +12,7 @@ import com.tangem.blockchain.common.WalletManagerFactory
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
import com.tangem.core.analytics.Analytics
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.datasource.api.common.MoshiConverter
import com.tangem.datasource.asset.AssetReader
import com.tangem.datasource.config.ConfigManager
@ -50,10 +51,8 @@ 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
import com.tangem.tap.proxy.redux.DaggerGraphAction
import com.tangem.tap.proxy.redux.DaggerGraphState
import com.tangem.wallet.BuildConfig
import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.launch
@ -66,7 +65,7 @@ lateinit var store: Store<AppState>
lateinit var foregroundActivityObserver: ForegroundActivityObserver
lateinit var activityResultCaller: ActivityResultCaller
lateinit var preferencesStorage: PreferencesStorage
lateinit var preferencesStorage: PreferencesDataSource
lateinit var walletConnectRepository: WalletConnectRepository
lateinit var shopService: TangemShopService
lateinit var userTokensRepository: UserTokensRepository
@ -129,10 +128,10 @@ class TapApplication : Application(), ImageLoaderFactory {
lateinit var networkConnectionManager: NetworkConnectionManager
@Inject
lateinit var tokensListFeatureToggles: TokensListFeatureToggles
lateinit var customTokenFeatureToggles: CustomTokenFeatureToggles
@Inject
lateinit var customTokenFeatureToggles: CustomTokenFeatureToggles
lateinit var preferencesDataSource: PreferencesDataSource
override fun onCreate() {
super.onCreate()
@ -142,7 +141,13 @@ class TapApplication : Application(), ImageLoaderFactory {
appReducer(action, state, appStateHolder)
},
middleware = AppState.getMiddleware(),
state = AppState(),
state = AppState(
daggerGraphState = DaggerGraphState(
assetReader = assetReader,
networkConnectionManager = networkConnectionManager,
customTokenFeatureToggles = customTokenFeatureToggles,
),
),
)
if (BuildConfig.DEBUG) {
@ -154,7 +159,7 @@ class TapApplication : Application(), ImageLoaderFactory {
registerActivityLifecycleCallbacks(foregroundActivityObserver.callbacks)
DomainLayer.init()
preferencesStorage = PreferencesStorage(this)
preferencesStorage = preferencesDataSource
walletConnectRepository = WalletConnectRepository(this)
val configLoader = FeaturesLocalLoader(assetReader, MoshiConverter.sdkMoshi, BuildConfig.ENVIRONMENT)
@ -178,15 +183,6 @@ class TapApplication : Application(), ImageLoaderFactory {
appStateHolder.userTokensRepository = userTokensRepository
appStateHolder.walletStoresManager = walletStoresManager
store.dispatch(
action = DaggerGraphAction.SetApplicationDependencies(
assetReader = assetReader,
networkConnectionManager = networkConnectionManager,
tokensListFeatureToggles = tokensListFeatureToggles,
customTokenFeatureToggles = customTokenFeatureToggles,
),
)
scope.launch {
featureTogglesManager.init()
}
@ -248,7 +244,7 @@ class TapApplication : Application(), ImageLoaderFactory {
private fun initFeedbackManager(
context: Context,
preferencesStorage: PreferencesStorage,
preferencesStorage: PreferencesDataSource,
foregroundActivityObserver: ForegroundActivityObserver,
store: Store<AppState>,
) {

View file

@ -7,32 +7,15 @@ import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.ui.SimpleAlertDialog
import com.tangem.tap.common.ui.SimpleCancelableAlertDialog
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ApproveWcSessionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.BnbTransactionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ChooseNetworkDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.ClipboardOrScanQrDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.PersonalSignDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.TransactionDialog
import com.tangem.tap.features.details.ui.walletconnect.dialogs.*
import com.tangem.tap.features.onboarding.AddressInfoBottomSheetDialog
import com.tangem.tap.features.onboarding.OnboardingDialog
import com.tangem.tap.features.onboarding.products.twins.ui.dialog.TwinningProcessNotCompletedDialog
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.InterruptOnboardingDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.NoFundsForActivationDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.PutVisaCardDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.RegistrationErrorDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AddMoreBackupCardsDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.BackupInProgressDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.ConfirmDiscardingBackupDialog
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.UnfinishedBackupFoundDialog
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.*
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.*
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ChooseTradeActionBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.RussianCardholdersWarningBottomSheetDialog
import com.tangem.tap.features.wallet.ui.dialogs.ScanFailsDialog
import com.tangem.tap.features.wallet.ui.dialogs.SignedHashesWarningDialog
import com.tangem.tap.features.wallet.ui.dialogs.SimpleOkDialog
import com.tangem.tap.features.wallet.ui.dialogs.*
import com.tangem.tap.features.wallet.ui.wallet.CurrencySelectionDialog
import com.tangem.tap.store
import com.tangem.wallet.R
@ -131,6 +114,10 @@ class DialogManager : StoreSubscriber<GlobalState> {
is BackupDialog.BackupInProgress -> BackupInProgressDialog.create(context)
is BackupDialog.UnfinishedBackupFound -> UnfinishedBackupFoundDialog.create(context)
is BackupDialog.ConfirmDiscardingBackup -> ConfirmDiscardingBackupDialog.create(context)
is BackupDialog.ResetBackupCard -> ResetBackupCardDialog.create(
context = context,
cardId = state.dialog.cardId,
)
is SaltPayDialog.Activation.NoGas -> NoFundsForActivationDialog.create(context)
is SaltPayDialog.Activation.PutVisaCard -> PutVisaCardDialog.create(context)
is SaltPayDialog.Activation.OnError -> RegistrationErrorDialog.create(context, state.dialog)

View file

@ -31,8 +31,8 @@ private class ThrowableErrorConverter : Converter<Throwable, Map<String, String>
override fun convert(value: Throwable): Map<String, String> {
val unknown = "unknown"
return mapOf(
AnalyticsParam.ErrorKey to AnalyticsParam.Error.App.value,
AnalyticsParam.ErrorDescription to (value.message ?: value.cause?.message ?: unknown),
AnalyticsParam.ERROR_KEY to AnalyticsParam.Error.App.value,
AnalyticsParam.ERROR_DESCRIPTION to (value.message ?: value.cause?.message ?: unknown),
)
}
}
@ -43,9 +43,9 @@ class CardSdkErrorConverter : Converter<TangemSdkError, Map<String, String>> {
if (value is TangemSdkError.UserCancelled) return emptyMap()
return mapOf(
AnalyticsParam.ErrorKey to AnalyticsParam.Error.CardSdk.value,
AnalyticsParam.ErrorCode to value.code.toString(),
AnalyticsParam.ErrorDescription to value.toString(),
AnalyticsParam.ERROR_KEY to AnalyticsParam.Error.CardSdk.value,
AnalyticsParam.ERROR_CODE to value.code.toString(),
AnalyticsParam.ERROR_DESCRIPTION to value.toString(),
)
}
}
@ -62,9 +62,9 @@ private class BlockchainSdkErrorConverter(
}
return mapOf(
AnalyticsParam.ErrorKey to AnalyticsParam.Error.BlockchainSdk.value,
AnalyticsParam.ErrorCode to value.code.toString(),
AnalyticsParam.ErrorDescription to "${value.javaClass.simpleName}: ${value.customMessage}",
AnalyticsParam.ERROR_KEY to AnalyticsParam.Error.BlockchainSdk.value,
AnalyticsParam.ERROR_CODE to value.code.toString(),
AnalyticsParam.ERROR_DESCRIPTION to "${value.javaClass.simpleName}: ${value.customMessage}",
)
}
}

View file

@ -93,15 +93,22 @@ sealed class AnalyticsParam {
object Sell : TxSentFrom("Sell")
}
sealed class WalletCreationType(val value: String) {
object PrivateKey : WalletCreationType("Private key")
object NewSeed : WalletCreationType("New seed")
object SeedImport : WalletCreationType("Seed import")
}
companion object Key {
const val Source = "Source"
const val Balance = "Balance"
const val Batch = "Batch"
const val ProductType = "Product Type"
const val Firmware = "Firmware"
const val Currency = "Currency"
const val ErrorDescription = "Error Description"
const val ErrorCode = "Error Code"
const val ErrorKey = "Error Key"
const val SOURCE = "Source"
const val BALANCE = "Balance"
const val BATCH = "Batch"
const val PRODUCT_TYPE = "Product Type"
const val FIRMWARE = "Firmware"
const val CURRENCY = "Currency"
const val ERROR_DESCRIPTION = "Error Description"
const val ERROR_CODE = "Error Code"
const val ERROR_KEY = "Error Key"
const val CREATION_TYPE = "Creation type"
}
}

View file

@ -14,7 +14,7 @@ sealed class Basic(
class BalanceLoaded(balance: AnalyticsParam.CardBalanceState) : Basic(
event = "Balance Loaded",
params = mapOf(
AnalyticsParam.Balance to balance.value,
AnalyticsParam.BALANCE to balance.value,
),
)
@ -23,7 +23,7 @@ sealed class Basic(
) : Basic(
event = "Card Was Scanned",
params = mapOf(
AnalyticsParam.Source to source.value,
AnalyticsParam.SOURCE to source.value,
),
)
@ -34,8 +34,8 @@ sealed class Basic(
) : Basic(
event = "Signed in",
params = mapOf(
AnalyticsParam.Currency to currency.value,
AnalyticsParam.Batch to batch,
AnalyticsParam.CURRENCY to currency.value,
AnalyticsParam.BATCH to batch,
"Sign in type" to signInType.name,
),
) {
@ -46,12 +46,12 @@ sealed class Basic(
class ToppedUp(currency: AnalyticsParam.CardCurrency) : Basic(
event = "Topped up",
params = mapOf(AnalyticsParam.Currency to currency.value),
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
)
class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom) : Basic(
event = "Transaction sent",
params = mapOf(AnalyticsParam.Source to sentFrom.value),
params = mapOf(AnalyticsParam.SOURCE to sentFrom.value),
)
class ScanError(error: Throwable) : Basic(

View file

@ -21,7 +21,12 @@ sealed class Onboarding(
class ScreenOpened : CreateWallet("Create Wallet Screen Opened")
class ButtonCreateWallet : CreateWallet("Button - Create Wallet")
class WalletCreatedSuccessfully : CreateWallet("Wallet Created Successfully")
class WalletCreatedSuccessfully(
creationType: AnalyticsParam.WalletCreationType = AnalyticsParam.WalletCreationType.PrivateKey,
) : CreateWallet(
event = "Wallet Created Successfully",
params = mapOf(AnalyticsParam.CREATION_TYPE to creationType.value),
)
}
sealed class Backup(
@ -51,7 +56,7 @@ sealed class Onboarding(
class ButtonBuyCrypto(currency: AnalyticsParam.CurrencyType) : Topup(
event = "Button - Buy Crypto",
params = mapOf(AnalyticsParam.Currency to currency.value),
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
)
class ButtonShowWalletAddress : Topup("Button - Show the Wallet Address")

View file

@ -2,8 +2,8 @@ package com.tangem.tap.common.analytics.paramsInterceptor
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.core.analytics.api.ParamsInterceptor
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -31,12 +31,12 @@ class CardContextInterceptor(
scanResponse ?: return
val card = scanResponse.card
params[AnalyticsParam.Batch] = card.batchId
params[AnalyticsParam.ProductType] = getProductType(scanResponse)
params[AnalyticsParam.Firmware] = card.firmwareVersion.stringValue
params[AnalyticsParam.BATCH] = card.batchId
params[AnalyticsParam.PRODUCT_TYPE] = getProductType(scanResponse)
params[AnalyticsParam.FIRMWARE] = card.firmwareVersion.stringValue
ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)?.let {
params[AnalyticsParam.Currency] = it.value
params[AnalyticsParam.CURRENCY] = it.value
}
}

View file

@ -3,9 +3,11 @@ package com.tangem.tap.common.analytics.topup
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.isZero
import com.tangem.core.analytics.Analytics
import com.tangem.data.source.preferences.model.DataSourceTopupInfo
import com.tangem.data.source.preferences.storage.ToppedUpWalletStorage
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.UserWalletId
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.converters.TopUpEventConverter
import com.tangem.tap.common.analytics.events.AnalyticsParam
@ -19,7 +21,6 @@ import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.domain.walletCurrencies.WalletCurrenciesManager
import com.tangem.tap.domain.walletStores.WalletStoresManager
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.persistence.ToppedUpWalletStorage
import com.tangem.tap.scope
import kotlinx.coroutines.launch
import java.math.BigDecimal
@ -90,9 +91,9 @@ class TopUpController(
val isToppedUpInPast = findToppedUpCurrenciesInPast(walletDataModels).isNotEmpty()
if (isToppedUpInPast) {
val newWalletInfo = ToppedUpWalletStorage.TopupInfo(
val newWalletInfo = DataSourceTopupInfo(
walletId = userWalletId.stringValue,
cardBalanceState = AnalyticsParam.CardBalanceState.Full,
cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full,
)
topupWalletStorage.save(newWalletInfo)
return@launch
@ -109,9 +110,9 @@ class TopUpController(
fun registerEmptyWallet(scanResponse: ScanResponse) {
UserWalletIdBuilder.scanResponse(scanResponse).build()?.let {
topupWalletStorage.save(
ToppedUpWalletStorage.TopupInfo(
DataSourceTopupInfo(
walletId = it.stringValue,
cardBalanceState = AnalyticsParam.CardBalanceState.Empty,
cardBalanceState = DataSourceTopupInfo.CardBalanceState.Empty,
),
)
}
@ -129,17 +130,28 @@ class TopUpController(
cardTypesResolver: CardTypesResolver,
) {
val topupInfo = topupWalletStorage.restore(userWalletId.stringValue).guard {
val topupInfo = ToppedUpWalletStorage.TopupInfo(
val topupInfo = DataSourceTopupInfo(
walletId = userWalletId.stringValue,
cardBalanceState = cardBalanceState,
cardBalanceState = when (cardBalanceState) {
AnalyticsParam.CardBalanceState.BlockchainError ->
DataSourceTopupInfo.CardBalanceState.BlockchainError
AnalyticsParam.CardBalanceState.CustomToken ->
DataSourceTopupInfo.CardBalanceState.CustomToken
AnalyticsParam.CardBalanceState.Empty ->
DataSourceTopupInfo.CardBalanceState.Empty
AnalyticsParam.CardBalanceState.Full ->
DataSourceTopupInfo.CardBalanceState.Full
},
)
topupWalletStorage.save(topupInfo)
return
}
if (topupInfo.isToppedUp) return
if (!topupInfo.isToppedUp && cardBalanceState.isToppedUp()) {
topupWalletStorage.save(topupInfo.copy(cardBalanceState = AnalyticsParam.CardBalanceState.Full))
val isToppedUp = topupInfo.cardBalanceState == DataSourceTopupInfo.CardBalanceState.Full
if (isToppedUp) return
if (cardBalanceState.isToppedUp()) {
topupWalletStorage.save(topupInfo.copy(cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full))
TopUpEventConverter().convert(cardTypesResolver)?.let {
Analytics.send(it)
}

View file

@ -2,19 +2,19 @@ package com.tangem.tap.common.chat
import android.content.Context
import android.os.Build
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.datasource.config.models.ChatConfig
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.datasource.config.models.SprinklrConfig
import com.tangem.datasource.config.models.ZendeskConfig
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.tap.ForegroundActivityObserver
import com.tangem.tap.common.chat.opener.ChatOpener
import com.tangem.tap.common.chat.opener.implementation.SprinklrChatOpener
import com.tangem.tap.common.chat.opener.implementation.ZendeskChatOpener
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.persistence.PreferencesStorage
import org.rekotlin.Store
class ChatManager(
private val preferencesStorage: PreferencesStorage,
private val preferencesStorage: PreferencesDataSource,
private val foregroundActivityObserver: ForegroundActivityObserver,
private val store: Store<AppState>,
) {

View file

@ -1,71 +0,0 @@
package com.tangem.tap.common.compose
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.sp
import com.tangem.wallet.R
object TangemTypography {
val body1 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontSize = 16.0.sp,
letterSpacing = 0.5.sp,
lineHeight = 24.0.sp,
)
val body2 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontSize = 14.0.sp,
letterSpacing = 0.25.sp,
lineHeight = 20.0.sp,
)
val button = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 14.0.sp,
letterSpacing = 0.10000000149011612.sp,
lineHeight = 20.0.sp,
)
val caption = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontSize = 12.0.sp,
letterSpacing = 0.4000000059604645.sp,
lineHeight = 16.0.sp,
)
val headline1 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_regular)),
fontSize = 34.0.sp,
letterSpacing = 0.0.sp,
lineHeight = 44.0.sp,
)
val headline2 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 24.0.sp,
letterSpacing = 0.18000000715255737.sp,
lineHeight = 32.0.sp,
)
val headline3 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 20.0.sp,
letterSpacing = 0.15000000596046448.sp,
lineHeight = 24.0.sp,
)
val overline = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 10.0.sp,
letterSpacing = 1.5.sp,
lineHeight = 16.0.sp,
)
val subtitle1 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 16.0.sp,
letterSpacing = 0.15000000596046448.sp,
lineHeight = 24.0.sp,
)
val subtitle2 = TextStyle(
fontFamily = FontFamily(Font(R.font.roboto_medium)),
fontSize = 14.0.sp,
letterSpacing = 0.10000000149011612.sp,
lineHeight = 24.0.sp,
)
}

View file

@ -40,6 +40,10 @@ fun Blockchain.getGreyedOutIconRes(): Int {
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
Blockchain.Cosmos, Blockchain.CosmosTestnet -> R.drawable.ic_cosmos_no_color
Blockchain.TerraV1 -> R.drawable.ic_terra_no_color
Blockchain.TerraV2 -> R.drawable.ic_terra2_no_color
Blockchain.Cronos -> R.drawable.ic_cronos_no_color
else -> R.drawable.ic_tangem_logo
}
}

View file

@ -1,15 +1,5 @@
package com.tangem.tap.common.extensions
/**
[REDACTED_AUTHOR]
*/
fun <T> List<T>.containsAny(list: List<T>): Boolean {
this.forEach { mainItem ->
list.forEach { if (it == mainItem) return true }
}
return false
}
fun <T> MutableList<T>.removeBy(predicate: (T) -> Boolean): Boolean {
val toRemove = this.filter(predicate)
this.removeAll(toRemove)

View file

@ -5,6 +5,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import com.tangem.feature.referral.ReferralFragment
import com.tangem.feature.swap.presentation.SwapFragment
import com.tangem.tap.common.redux.navigation.AppScreen
@ -28,7 +29,6 @@ import com.tangem.tap.features.saveWallet.ui.SaveWalletBottomSheetFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.shop.ui.ShopFragment
import com.tangem.tap.features.tokens.impl.presentation.TokensListFragment
import com.tangem.tap.features.tokens.legacy.AddTokensFragment
import com.tangem.tap.features.wallet.ui.WalletDetailsFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.walletSelector.ui.WalletSelectorBottomSheetFragment
@ -45,24 +45,35 @@ fun FragmentActivity.openFragment(
bundle: Bundle? = null,
fgShareTransition: FragmentShareTransition? = null,
) {
val transaction = this.supportFragmentManager.beginTransaction()
val fragment = fragmentFactory(screen)
fragment.arguments = bundle
fgShareTransition?.apply {
fragment.sharedElementEnterTransition = enterTransitionSet
fragment.sharedElementReturnTransition = exitTransitionSet
transaction.setReorderingAllowed(true)
shareElements.forEach { shareElement ->
shareElement.wView.get()?.let { view ->
transaction.addSharedElement(view, shareElement.elementName)
val transaction = supportFragmentManager.beginTransaction().apply {
setReorderingAllowed(true)
}
val fragment = fragmentFactory(screen).apply {
arguments = bundle
if (fgShareTransition != null) {
sharedElementEnterTransition = fgShareTransition.enterTransitionSet
sharedElementReturnTransition = fgShareTransition.exitTransitionSet
fgShareTransition.shareElements.forEach { shareElement ->
shareElement.wView.get()?.let { view ->
transaction.addSharedElement(view, shareElement.elementName)
}
}
}
}
if (screen.isDialogFragment) {
(fragment as DialogFragment).show(transaction, screen.name)
if (addToBackstack) {
transaction.addToBackStack(screen.name)
val dialogFragment = requireNotNull(fragment as? DialogFragment) {
"If screen.isDialogFragment == true then fragment must be a DialogFragment"
}
dialogFragment.showAllowingStateLoss(
fragmentManager = supportFragmentManager,
baseTransaction = transaction,
tag = screen.name,
addToBackstack = addToBackstack,
)
} else {
transaction.replace(R.id.fragment_container, fragment, screen.name)
if (addToBackstack) {
@ -72,6 +83,29 @@ fun FragmentActivity.openFragment(
}
}
private fun DialogFragment.showAllowingStateLoss(
fragmentManager: FragmentManager,
baseTransaction: FragmentTransaction,
tag: String,
addToBackstack: Boolean,
) {
runCatching {
if (addToBackstack) baseTransaction.addToBackStack(tag)
show(baseTransaction, tag)
}
.onFailure { throwable ->
if (throwable is IllegalStateException) {
val transaction = fragmentManager.beginTransaction()
transaction.add(this, tag)
if (addToBackstack) transaction.addToBackStack(tag)
transaction.commitAllowingStateLoss()
} else {
Timber.e(throwable)
}
}
}
fun FragmentActivity.popBackTo(screen: AppScreen?, inclusive: Boolean = false) {
val inclusiveFlag = if (inclusive) FragmentManager.POP_BACK_STACK_INCLUSIVE else 0
try {
@ -113,12 +147,8 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.ResetToFactory -> ResetCardFragment()
AppScreen.AccessCodeRecovery -> AccessCodeRecoveryFragment()
AppScreen.Disclaimer -> DisclaimerFragment()
AppScreen.AddTokens -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::tokensListFeatureToggles,
)
if (featureToggles.isRedesignedScreenEnabled) TokensListFragment() else AddTokensFragment()
}
AppScreen.AddTokens -> TokensListFragment()
AppScreen.AddCustomToken -> {
val featureToggles = store.state.daggerGraphState.get(
getDependency = DaggerGraphState::customTokenFeatureToggles,
@ -129,6 +159,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AddCustomTokenFragment()
}
}
AppScreen.WalletDetails -> WalletDetailsFragment()
AppScreen.WalletConnectSessions -> WalletConnectFragment()
AppScreen.QrScan -> QrScanFragment()

View file

@ -101,7 +101,7 @@ data class AppState(
SendMiddleware().sendMiddleware,
DetailsMiddleware().detailsMiddleware,
DisclaimerMiddleware().disclaimerMiddleware,
TokensMiddleware().tokensMiddleware,
TokensMiddleware.tokensMiddleware,
WalletConnectMiddleware().walletConnectMiddleware,
BackupMiddleware().backupMiddleware,
ShopMiddleware().shopMiddleware,

View file

@ -5,11 +5,12 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.datasource.config.models.Config
import com.tangem.domain.common.LogConfig
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
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.entities.FiatCurrency
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -75,7 +76,9 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(
GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency(),
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
?.run { FiatCurrency(code, name, symbol) }
?: FiatCurrency.Default,
),
)
}

View file

@ -5,22 +5,14 @@ import androidx.annotation.StringRes
import com.tangem.Message
import com.tangem.TangemSdk
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CardFilter
import com.tangem.common.CompletionResult
import com.tangem.common.SuccessResponse
import com.tangem.common.UserCode
import com.tangem.common.UserCodeType
import com.tangem.common.*
import com.tangem.common.biometric.BiometricManager
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.Config
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.UserCodeRequestPolicy
import com.tangem.common.core.*
import com.tangem.common.extensions.ByteArrayKey
import com.tangem.common.map
import com.tangem.common.usersCode.UserCodeRepository
import com.tangem.core.analytics.Analytics
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
@ -45,6 +37,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
import kotlin.coroutines.resume
@Suppress("TooManyFunctions")
class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Context) {
private val userCodeRepository by lazy {
@ -91,6 +84,21 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
)
}
suspend fun importWallet(
scanResponse: ScanResponse,
mnemonic: String,
): CompletionResult<CreateProductWalletTaskResponse> {
return when (val seedResult = DefaultMnemonic(mnemonic, tangemSdk.wordlist).generateSeed()) {
is CompletionResult.Success -> runTaskAsync(
CreateProductWalletTask(scanResponse.cardTypesResolver, seedResult.data),
scanResponse.card.cardId,
Message(context.getString(R.string.initial_message_create_wallet_body)),
)
is CompletionResult.Failure -> CompletionResult.Failure(seedResult.error)
}
}
private fun sendScanResultsToAnalytics(result: CompletionResult<ScanResponse>) {
if (result is CompletionResult.Failure) {
(result.error as? TangemSdkError)?.let { error ->

View file

@ -1,6 +1,10 @@
package com.tangem.tap.domain
import com.tangem.blockchain.common.*
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.common.doOnFailure
import com.tangem.common.doOnSuccess
import com.tangem.core.analytics.Analytics
@ -11,6 +15,7 @@ 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
import com.tangem.tap.common.extensions.dispatchWithMain
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.model.UserWallet
@ -24,17 +29,39 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.walletStoresManager
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import timber.log.Timber
class TapWalletManager {
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
class TapWalletManager(
private val dispatchers: CoroutineDispatcherProvider = AppCoroutineDispatcherProvider(),
) {
private val blockchainSdkConfig by lazy {
store.state.globalState.configManager?.config?.blockchainSdkConfig ?: BlockchainSdkConfig()
}
private var loadUserWalletDataJob: Job? = null
set(value) {
field?.cancel()
field = value
}
val walletManagerFactory: WalletManagerFactory
by lazy { WalletManagerFactory(blockchainSdkConfig) }
suspend fun onWalletSelected(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) {
// If a previous job was running, it gets cancelled before the new one starts,
// ensuring that only one job is active at any given time.
loadUserWalletDataJob = CoroutineScope(dispatchers.io)
.launch { loadUserWalletData(userWallet, refresh, sendAnalyticsEvent) }
.also { it.join() }
}
private suspend fun loadUserWalletData(userWallet: UserWallet, refresh: Boolean, sendAnalyticsEvent: Boolean) {
Analytics.setContext(userWallet.scanResponse)
if (sendAnalyticsEvent) {
Analytics.send(Basic.WalletOpened())
@ -68,6 +95,7 @@ class TapWalletManager {
Timber.d("Wallet stores fetched for ${userWallet.walletId}")
store.dispatchOnMain(WalletAction.LoadData.Success)
store.state.globalState.topUpController?.loadDataSuccess()
store.dispatchWithMain(WalletAction.Warnings.CheckHashesCount.VerifyOnlineIfNeeded)
}
.doOnFailure { error ->
val errorAction = when (error) {

View file

@ -1,16 +1,16 @@
package com.tangem.tap.domain.configurable.warningMessage
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.extensions.containsAny
import com.tangem.tap.common.extensions.removeBy
import com.tangem.wallet.R
import java.util.concurrent.CopyOnWriteArrayList
/**
[REDACTED_AUTHOR]
*/
class WarningMessagesManager {
private val warningsList: MutableList<WarningMessage> = mutableListOf()
private val warningsList = CopyOnWriteArrayList<WarningMessage>()
fun addWarning(warning: WarningMessage) {
if (findWarning(warning) == null) {
@ -19,36 +19,27 @@ class WarningMessagesManager {
}
}
fun getWarnings(
location: WarningMessage.Location,
forBlockchains: List<Blockchain> = emptyList(),
): List<WarningMessage> {
return warningsList
.filter { !it.isHidden && it.location.contains(location) }
.filter {
val list = it.blockchainList
when {
list == null -> true
list.containsAny(forBlockchains) -> true
else -> false
}
}
fun getWarnings(location: WarningMessage.Location, blockchains: List<Blockchain>): List<WarningMessage> {
return warningsList.filter { message ->
val messageBlockchains = message.blockchainList
val isCorrespondingMessageBlockchains = messageBlockchains == null ||
messageBlockchains.any(blockchains::contains)
val isCorrespondingMessageLocation = message.location.contains(location)
!message.isHidden && isCorrespondingMessageLocation && isCorrespondingMessageBlockchains
}
}
fun hideWarning(warning: WarningMessage): Boolean {
val foundWarning = findWarning(warning)
return when {
foundWarning == null -> false
foundWarning.type == WarningMessage.Type.Temporary ||
foundWarning.type == WarningMessage.Type.AppRating -> {
if (foundWarning.isHidden) {
false
} else {
foundWarning.isHidden = true
true
}
}
else -> false
val foundWarning = findWarning(warning) ?: return false
val isCorrectType = foundWarning.type == WarningMessage.Type.Temporary ||
foundWarning.type == WarningMessage.Type.AppRating
return if (!foundWarning.isHidden && isCorrectType) {
foundWarning.isHidden = true
true
} else {
false
}
}
@ -61,6 +52,8 @@ class WarningMessagesManager {
warningsList.removeBy { it.messageResId == messageRes }
}
fun containsWarning(warning: WarningMessage) = warning in warningsList
private fun sortByPriority() {
warningsList.sortBy { it.priority.ordinal }
}
@ -72,31 +65,31 @@ class WarningMessagesManager {
companion object {
const val REMAINING_SIGNATURES_WARNING = 10
fun devCardWarning(): WarningMessage = WarningMessage(
"",
"",
val devCardWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.common_warning,
R.string.alert_developer_card,
WarningMessage.Origin.Local,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.common_warning,
messageResId = R.string.alert_developer_card,
origin = WarningMessage.Origin.Local,
)
fun alreadySignedHashesWarning(): WarningMessage = WarningMessage(
"",
"",
val alreadySignedHashesWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Temporary,
priority = WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.common_warning,
R.string.alert_card_signed_transactions,
WarningMessage.Origin.Local,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.common_warning,
messageResId = R.string.alert_card_signed_transactions,
origin = WarningMessage.Origin.Local,
)
fun signedHashesMultiWalletWarning(): WarningMessage = WarningMessage(
val signedHashesMultiWalletWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Temporary,
@ -110,69 +103,71 @@ class WarningMessagesManager {
titleFormatArg = "\u26A0",
)
fun appRatingWarning(): WarningMessage = WarningMessage(
"",
"",
WarningMessage.Type.AppRating,
WarningMessage.Priority.Info,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_rate_app_title,
R.string.warning_rate_app_message,
WarningMessage.Origin.Local,
val appRatingWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.AppRating,
priority = WarningMessage.Priority.Info,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.warning_rate_app_title,
messageResId = R.string.warning_rate_app_message,
origin = WarningMessage.Origin.Local,
)
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
return warning.messageResId == R.string.alert_card_signed_transactions
}
fun onlineVerificationFailed(): WarningMessage = WarningMessage(
"",
"",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.warning_failed_to_verify_card_title,
R.string.warning_failed_to_verify_card_message,
WarningMessage.Origin.Local,
)
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage = WarningMessage(
val onlineVerificationFailed = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.common_warning,
messageResId = R.string.warning_low_signatures_format,
titleResId = R.string.warning_failed_to_verify_card_title,
messageResId = R.string.warning_failed_to_verify_card_message,
origin = WarningMessage.Origin.Local,
messageFormatArg = remainingSignatures.toString(),
)
fun testCardWarning(): WarningMessage = WarningMessage(
"",
"",
val testCardWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.TestCard,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen, WarningMessage.Location.SendScreen),
null,
R.string.common_warning,
R.string.warning_testnet_card_message,
WarningMessage.Origin.Local,
location = listOf(WarningMessage.Location.MainScreen, WarningMessage.Location.SendScreen),
blockchains = null,
titleResId = R.string.common_warning,
messageResId = R.string.warning_testnet_card_message,
origin = WarningMessage.Origin.Local,
)
fun demoCardWarning(): WarningMessage = WarningMessage(
"",
"",
val demoCardWarning = WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
listOf(WarningMessage.Location.MainScreen),
null,
R.string.common_warning,
R.string.alert_demo_message,
WarningMessage.Origin.Local,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.common_warning,
messageResId = R.string.alert_demo_message,
origin = WarningMessage.Origin.Local,
)
fun remainingSignaturesNotEnough(remainingSignatures: Int): WarningMessage {
return WarningMessage(
title = "",
message = "",
type = WarningMessage.Type.Permanent,
priority = WarningMessage.Priority.Critical,
location = listOf(WarningMessage.Location.MainScreen),
blockchains = null,
titleResId = R.string.common_warning,
messageResId = R.string.warning_low_signatures_format,
origin = WarningMessage.Origin.Local,
messageFormatArg = remainingSignatures.toString(),
)
}
fun isAlreadySignedHashesWarning(warning: WarningMessage): Boolean {
return warning.messageResId == R.string.alert_card_signed_transactions
}
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.right
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.AnalyticsEvent
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.analytics.paramsInterceptor.CardContextInterceptor
/**
* Handles the analytics event interception and sending event after the scan card operation. Always returns result of
* previous chain.
*
* @param event the analytics event to be handled by this chain.
*
* @see Chain for more information about the Chain interface.
*/
class AnalyticsChain(
private val event: AnalyticsEvent,
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
val interceptor = CardContextInterceptor(previousChainResult)
val params = event.params.toMutableMap()
interceptor.intercept(params)
event.params = params.toMap()
Analytics.send(event)
return previousChainResult.right()
}
}

View file

@ -0,0 +1,126 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.common.services.Result
import com.tangem.core.analytics.Analytics
import com.tangem.data.source.preferences.PreferencesDataSource
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.twinsIsTwinned
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.extensions.addContext
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.setContext
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.features.onboarding.OnboardingHelper
import com.tangem.tap.features.onboarding.OnboardingSaltPayHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivationManagerFactory
import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayExceptionHandler
import com.tangem.tap.features.onboarding.products.wallet.saltPay.message.SaltPayActivationError
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import kotlinx.coroutines.delay
import org.rekotlin.Store
/**
* Handles the verification process to determine if the scanned card requires onboarding.
*
* Returns:
* - [ScanChainException.OnboardingNeeded] if onboarding required for none SaltPay cards.
* - [ScanChainException.CheckForSaltPayOnboardingCaseException] if unable to check for SaltPay onboarding case.
* - [ScanChainException.PutSaltPayVisaCard] if scanned SaltPay card is not activated and onboarding required.
*
* @param store the [Store] that holds the state of the app.
* @param tapWalletManager manager responsible for handling operations related to the Wallet.
* @param preferencesStore data source for user preferences.
*
* @see Chain for more information about the Chain interface.
*/
class CheckForOnboardingChain(
private val store: Store<AppState>,
private val tapWalletManager: TapWalletManager,
private val preferencesStore: PreferencesDataSource,
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
val cardTypesResolver = previousChainResult.cardTypesResolver
tapWalletManager.updateConfigManager(previousChainResult)
store.dispatchOnMain(TwinCardsAction.IfTwinsPrepareState(previousChainResult))
return when {
cardTypesResolver.isSaltPayVisa() -> {
val manager = SaltPayActivationManagerFactory(
blockchain = previousChainResult.cardTypesResolver.getBlockchain(),
card = previousChainResult.card,
).create()
val result = OnboardingSaltPayHelper.isOnboardingCase(previousChainResult, manager)
delay(timeMillis = 500)
withMainContext {
when (result) {
is Result.Success -> {
Analytics.addContext(previousChainResult)
val isOnboardingCase = result.data
if (isOnboardingCase) {
store.dispatch(
GlobalAction.Onboarding.Start(previousChainResult, canSkipBackup = false),
)
store.dispatch(OnboardingSaltPayAction.SetDependencies(manager))
store.dispatch(OnboardingSaltPayAction.Update(withAnalytics = false))
ScanChainException.OnboardingNeeded(AppScreen.OnboardingWallet).left()
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
previousChainResult.right()
}
}
is Result.Failure -> {
delay(DELAY_SDK_DIALOG_CLOSE)
SaltPayExceptionHandler.handle(result.error)
ScanChainException.CheckForSaltPayOnboardingCaseException(result.error).left()
}
}
}
}
cardTypesResolver.isSaltPayWallet() -> {
delay(DELAY_SDK_DIALOG_CLOSE)
if (previousChainResult.card.backupStatus?.isActive == false) {
SaltPayExceptionHandler.handle(SaltPayActivationError.PutVisaCard)
ScanChainException.PutSaltPayVisaCard.left()
} else {
Analytics.setContext(previousChainResult)
previousChainResult.right()
}
}
OnboardingHelper.isOnboardingCase(previousChainResult) -> {
Analytics.addContext(previousChainResult)
store.dispatchOnMain(GlobalAction.Onboarding.Start(previousChainResult, canSkipBackup = true))
val appScreen = OnboardingHelper.whereToNavigate(previousChainResult)
ScanChainException.OnboardingNeeded(appScreen).left()
}
else -> {
Analytics.setContext(previousChainResult)
// If twins was twinned previously but twins welcome not shown
if (previousChainResult.twinsIsTwinned() && !preferencesStore.wasTwinsOnboardingShown()) {
store.dispatchOnMain(
TwinCardsAction.SetStepOfScreen(TwinCardsStep.WelcomeOnly(previousChainResult)),
)
ScanChainException.OnboardingNeeded(AppScreen.OnboardingTwins).left()
} else {
delay(DELAY_SDK_DIALOG_CLOSE)
previousChainResult.right()
}
}
}
}
}

View file

@ -0,0 +1,44 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.backup.BackupService
import com.tangem.tap.common.extensions.primaryCardIsSaltPayVisa
/**
* Handles the verification for unfinished SaltPay backup processes after the card scanning operation.
*
* Returns [ScanChainException.PutSaltPayVisaCard] if backup is not finished.
*
* @param backupService a service responsible for managing backup operations, used here to check for unfinished backups
* and verify the primary card type.
*
* @see Chain for more information about the Chain interface.
*/
internal class CheckForUnfinishedSaltPayBackupChain(
private val backupService: BackupService,
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
if (!backupService.hasIncompletedBackup || !backupService.primaryCardIsSaltPayVisa()) {
return previousChainResult.right()
}
val isTheSamePrimaryCard = backupService.primaryCardId
?.let { it == previousChainResult.card.cardId }
?: false
return if (previousChainResult.cardTypesResolver.isSaltPayWallet() || !isTheSamePrimaryCard) {
ScanChainException.PutSaltPayVisaCard.left()
} else {
previousChainResult.right()
}
}
}

View file

@ -0,0 +1,75 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.features.disclaimer.Disclaimer
import com.tangem.tap.features.disclaimer.createDisclaimer
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.disclaimer.redux.DisclaimerCallback
import kotlinx.coroutines.suspendCancellableCoroutine
import org.rekotlin.Store
import kotlin.coroutines.resume
/**
* Handles disclaimer display after the card scanning operation.
*
* Returns [ScanChainException.DisclaimerWasCanceled] if the disclaimer is dismissed by the user.
*
* @param store the [Store] that holds the state of the app, used here to dispatch actions related to disclaimers.
* @param disclaimerWillShow an optional function to be invoked when a disclaimer is about to be shown. Default is an
* empty function.
*
* @see Chain for more information about the Chain interface.
*/
internal class DisclaimerChain(
private val store: Store<AppState>,
private val disclaimerWillShow: () -> Unit = {},
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(
previousChainResult: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
val disclaimer = previousChainResult.card.createDisclaimer()
return if (disclaimer.isAccepted()) {
previousChainResult.right()
} else {
disclaimerWillShow()
showDisclaimer(disclaimer, previousChainResult)
}
}
private suspend fun showDisclaimer(
disclaimer: Disclaimer,
response: ScanResponse,
): Either<ScanCardException.ChainException, ScanResponse> {
store.dispatchOnMain(DisclaimerAction.SetDisclaimer(disclaimer))
return suspendCancellableCoroutine { continuation ->
store.dispatchOnMain(
DisclaimerAction.Show(
fromScreen = AppScreen.Home,
callback = DisclaimerCallback(
onAccept = {
if (continuation.isActive) {
continuation.resume(response.right())
}
},
onDismiss = {
if (continuation.isActive) {
continuation.resume(ScanChainException.DisclaimerWasCanceled.left())
}
},
),
),
)
}
}
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.domain.scanCard.chains
import com.tangem.domain.card.ScanCardException
import com.tangem.tap.common.redux.navigation.AppScreen
sealed class ScanChainException : ScanCardException.ChainException() {
/**
* May be returned from [CheckForOnboardingChain] and [CheckForUnfinishedSaltPayBackupChain]
* */
object PutSaltPayVisaCard : ScanChainException()
/**
* May be returned from [DisclaimerChain]
* */
object DisclaimerWasCanceled : ScanChainException()
/**
* May be returned from [CheckForOnboardingChain]
*
* @param onboardingRoute route where to navigate
* */
data class OnboardingNeeded(val onboardingRoute: AppScreen) : ScanChainException()
/**
* May be returned from [CheckForOnboardingChain]
*
* @param cause cause of exception
* */
data class CheckForSaltPayOnboardingCaseException(override val cause: Throwable?) : ScanChainException()
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.domain.scanCard.chains
import arrow.core.Either
import arrow.core.right
import com.tangem.domain.card.ScanCardException
import com.tangem.domain.core.chain.Chain
import com.tangem.domain.models.scan.ScanResponse
/**
* Responsible for invoking the callback at the end of the card scanning operation. Should be passed as last chain in
* after card scanning chains. Always returns result of previous chain.
*
* @param onScanningFinished a suspending function to be called when the scanning process has finished.
*
* @see Chain for more information about the Chain interface.
*/
internal class ScanningFinishedChain(
private val onScanningFinished: suspend () -> Unit,
) : Chain<ScanCardException.ChainException, ScanResponse> {
override suspend fun invoke(previousChainResult: ScanResponse): Either<ScanChainException, ScanResponse> {
onScanningFinished()
return previousChainResult.right()
}
}

View file

@ -10,12 +10,12 @@ import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ByteArrayKey
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.models.scan.CardDTO
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.CardTypesResolver
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.TapWorkarounds.isTestCard
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.KeyWalletPublicKey
import com.tangem.operations.CommandResponse
import com.tangem.operations.backup.PrimaryCard
@ -56,6 +56,7 @@ private data class CreateWalletResponse(
class CreateProductWalletTask(
private val cardTypesResolver: CardTypesResolver,
private val seed: ByteArray? = null,
) : CardSessionRunnable<CreateProductWalletTaskResponse> {
override val allowsRequestAccessCodeFromRepository: Boolean = false
@ -74,7 +75,8 @@ class CreateProductWalletTask(
cardTypesResolver.isTangemNote() -> CreateWalletTangemNote(cardTypesResolver)
cardTypesResolver.isTangemTwins() ->
throw UnsupportedOperationException("Use the TwinCardsManager to create a wallet")
else -> CreateWalletTangemWallet()
else -> CreateWalletTangemWallet(seed)
}
commandProcessor.proceed(cardDto, session) {
when (it) {
@ -129,7 +131,9 @@ private class CreateWalletTangemNote(private val cardTypesResolver: CardTypesRes
}
}
private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWalletTaskResponse> {
private class CreateWalletTangemWallet(
private val seed: ByteArray?,
) : ProductCommandProcessor<CreateProductWalletTaskResponse> {
private var primaryCard: PrimaryCard? = null
@ -138,7 +142,8 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
session: CardSession,
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
val curves = card.getCurvesForNonCreatedWallets()
val walletsOnCard = card.wallets.map { it.curve }.toSet()
val curves = card.supportedCurves.intersect(CURVES_FOR_WALLETS).subtract(walletsOnCard).toList()
if (curves.isEmpty()) {
val createWalletResponses = card.wallets.map { wallet ->
@ -148,7 +153,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
return
}
CreateWalletsTask(curves).run(session) { result ->
CreateWalletsTask(curves, seed).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
proceedWithCreatedWallets(
@ -158,7 +163,6 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
callback = callback,
)
}
is CompletionResult.Failure -> {
callback(CompletionResult.Failure(result.error))
}
@ -233,17 +237,38 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
callback: (result: CompletionResult<CreateProductWalletTaskResponse>) -> Unit,
) {
val map = mutableMapOf<ByteArrayKey, List<DerivationPath>>()
var isBlockchainsForCurvesExist = false
createWalletResponse.forEach { response ->
val blockchainsForCurve = getBlockchains(response.cardId, card).filter {
it.getSupportedCurves().contains(response.wallet.curve)
}
val derivationPaths = blockchainsForCurve.mapNotNull { it.derivationPath(card.derivationStyle) }
val derivationPaths = blockchainsForCurve.mapNotNull {
isBlockchainsForCurvesExist = true
it.derivationPath(card.derivationStyle)
}
if (derivationPaths.isNotEmpty()) {
map[response.wallet.publicKey.toMapKey()] = derivationPaths
}
}
val cardEnv = session.environment.card
if (cardEnv == null) {
callback(CompletionResult.Failure(TangemSdkError.CardError()))
return
}
if (map.isEmpty()) {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
if (isBlockchainsForCurvesExist) {
callback(CompletionResult.Failure(TangemSdkError.UnknownError()))
} else {
// if there is no blockchains to derive, just return success response with empty derivedKeys
callback(
CompletionResult.Success(
CreateProductWalletTaskResponse(
card = cardEnv,
primaryCard = primaryCard,
),
),
)
}
return
}
@ -254,7 +279,7 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
callback(
CompletionResult.Success(
CreateProductWalletTaskResponse(
card = session.environment.card!!,
card = cardEnv,
derivedKeys = result.data.entries,
primaryCard = primaryCard,
),
@ -274,4 +299,8 @@ private class CreateWalletTangemWallet : ProductCommandProcessor<CreateProductWa
else -> listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}
}
private companion object {
val CURVES_FOR_WALLETS = listOf(EllipticCurve.Secp256k1, EllipticCurve.Ed25519)
}
}

View file

@ -18,6 +18,7 @@ class CreateWalletsResponse(
class CreateWalletsTask(
private val curves: List<EllipticCurve>,
private val seed: ByteArray? = null,
) : CardSessionRunnable<CreateWalletsResponse> {
private val createdWalletsResponses = mutableListOf<CreateWalletResponse>()
@ -37,7 +38,7 @@ class CreateWalletsTask(
session: CardSession,
callback: (result: CompletionResult<CreateWalletsResponse>) -> Unit,
) {
CreateWalletTask(curve).run(session) { result ->
CreateWalletTask(curve, seed).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
createdWalletsResponses.add(result.data)
@ -47,6 +48,7 @@ class CreateWalletsTask(
}
createWallet(curves[createdWalletsResponses.size], session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.domain.tasks.product
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.Card
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
@ -19,9 +18,6 @@ import com.tangem.common.tlv.Tlv
import com.tangem.common.tlv.TlvDecoder
import com.tangem.crypto.CryptoUtils
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
@ -29,8 +25,9 @@ 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
import com.tangem.operations.PreflightReadMode
import com.tangem.operations.PreflightReadTask
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ProductType
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.operations.ScanTask
import com.tangem.operations.backup.PrimaryCard
import com.tangem.operations.backup.StartPrimaryCardLinkingTask
@ -39,7 +36,6 @@ import com.tangem.operations.files.ReadFilesTask
import com.tangem.operations.issuerAndUserData.ReadIssuerDataCommand
import com.tangem.tap.domain.TapSdkError
import com.tangem.tap.domain.extensions.getPrimaryCurve
import com.tangem.tap.domain.extensions.isFirmwareMultiwalletAllowed
import com.tangem.tap.domain.tokens.UserTokensRepository
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.preferencesStorage
@ -114,7 +110,7 @@ private class ScanWalletProcessor(
return
}
createMissingWalletsIfNeeded(card, session, callback)
startLinkingForBackupIfNeeded(card, session, callback)
}
private fun readFile(
@ -122,7 +118,7 @@ private class ScanWalletProcessor(
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val funToContinue = { createMissingWalletsIfNeeded(card, session, callback) }
val funToContinue = { startLinkingForBackupIfNeeded(card, session, callback) }
ReadFilesTask(fileName = "blockchainInfo", walletPublicKey = null).run(session) { result ->
@ -185,45 +181,6 @@ private class ScanWalletProcessor(
}
}
private fun createMissingWalletsIfNeeded(
card: CardDTO,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
if (card.wallets.isNotEmpty() && card.backupStatus?.isActive == true) {
startLinkingForBackupIfNeeded(card, session, callback)
return
}
if (card.wallets.isEmpty() || !card.isFirmwareMultiwalletAllowed) {
startLinkingForBackupIfNeeded(card, session, callback)
return
}
val curvesToCreate = card.getCurvesForNonCreatedWallets()
if (curvesToCreate.isEmpty()) {
startLinkingForBackupIfNeeded(card, session, callback)
return
}
CreateWalletsTask(curvesToCreate).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
PreflightReadTask(
readMode = PreflightReadMode.FullCardRead,
cardId = card.cardId,
).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> startLinkingForBackupIfNeeded(card, session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
}
}
}
private fun startLinkingForBackupIfNeeded(
card: CardDTO,
session: CardSession,
@ -424,10 +381,4 @@ private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
}
}
}
}
fun CardDTO.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
val curvesPresent = wallets.map { it.curve }.toSet()
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1)
return curvesForNonCreatedWallets.toList()
}

View file

@ -52,42 +52,36 @@ internal fun List<WalletDataModel>.updateWithAmounts(wallet: Wallet): List<Walle
internal fun WalletDataModel.updateWithAmount(wallet: Wallet): WalletDataModel {
val pendingTransactions = wallet.getPendingTransactions()
return this.copy(
status = when (val currency = this.currency) {
status = when (currency) {
is Currency.Blockchain -> {
val amount = wallet.fundsAvailable(AmountType.Coin)
if (pendingTransactions.isEmpty()) {
WalletDataModel.VerifiedOnline(
amount = amount,
)
WalletDataModel.VerifiedOnline(amount)
} else {
WalletDataModel.TransactionInProgress(
amount = amount,
pendingTransactions = pendingTransactions,
)
WalletDataModel.TransactionInProgress(amount, pendingTransactions)
}
}
is Currency.Token -> {
val token = currency.token
val amount = wallet.fundsAvailable(AmountType.Token(token))
val hasTokenPendingTransactions = pendingTransactions
.any { it.transactionData.amount.currencySymbol == token.symbol }
val hasTokenPendingTransactions = pendingTransactions.any {
it.transactionData.amount.currencySymbol == token.symbol
}
when {
hasTokenPendingTransactions -> {
WalletDataModel.TransactionInProgress(
amount = amount,
pendingTransactions = pendingTransactions,
)
WalletDataModel.TransactionInProgress(amount, pendingTransactions)
}
pendingTransactions.isNotEmpty() -> {
WalletDataModel.SameCurrencyTransactionInProgress(
amount = amount,
pendingTransactions = pendingTransactions,
)
// FIXME: Necessary to avoid passing pending transactions
// because SameCurrencyTransactionInProgress didn't use it. It used only to define main button
// availability. UI layer turned off pending transaction visibility for this state.
WalletDataModel.SameCurrencyTransactionInProgress(amount, pendingTransactions)
}
else -> {
WalletDataModel.VerifiedOnline(
amount = amount,
)
WalletDataModel.VerifiedOnline(amount)
}
}
}

View file

@ -16,6 +16,7 @@ object FoundTokenConverter : Converter<CoinsResponse.Coin, FoundToken> {
id = value.id,
name = value.name,
symbol = value.symbol,
isActive = value.active,
network = value.networks.firstOrNull()?.let { network ->
FoundToken.Network(
id = network.networkId,

View file

@ -1,7 +1,6 @@
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
@ -26,7 +25,6 @@ internal object CustomTokenInteractorModule {
tangemTechApi: TangemTechApi,
appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider,
reduxStateHolder: AppStateHolder,
derivationManager: DerivationManager,
): CustomTokenInteractor {
return DefaultCustomTokenInteractor(
featureRepository = DefaultCustomTokenRepository(
@ -34,7 +32,6 @@ internal object CustomTokenInteractorModule {
dispatchers = appCoroutineDispatcherProvider,
reduxStateHolder = reduxStateHolder,
),
derivationManager = derivationManager,
reduxStateHolder = reduxStateHolder,
)
}

View file

@ -1,8 +1,8 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.features.wallet.models.Currency
/**
* Custom token interactor
@ -14,6 +14,6 @@ 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)
/** Save token [customCurrency] */
suspend fun saveToken(customCurrency: CustomCurrency)
}

View file

@ -1,34 +1,40 @@
package com.tangem.tap.features.customtoken.impl.domain
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.card.EllipticCurve
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toMapKey
import com.tangem.common.flatMap
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.domain.common.TapWorkarounds.derivationStyle
import com.tangem.domain.common.extensions.toNetworkId
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.features.addCustomToken.CustomCurrency
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.operations.derivation.ExtendedPublicKeysMap
import com.tangem.tap.*
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
import com.tangem.tap.features.tokens.legacy.redux.TokensMiddleware
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 kotlinx.coroutines.delay
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 {
@ -39,58 +45,111 @@ class DefaultCustomTokenInteractor(
)
}
override suspend fun saveToken(currency: Currency, address: String) {
val hasDerivation = derivationManager.hasDerivation(
networkId = currency.blockchain.toNetworkId(),
derivationPath = requireNotNull(currency.derivationPath),
)
override suspend fun saveToken(customCurrency: CustomCurrency) {
val scanResponse = reduxStateHolder.scanResponse ?: return
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,
)
val currency = Currency.fromCustomCurrency(customCurrency)
val isNeedToDerive = isNeedToDerive(scanResponse, currency)
if (isNeedToDerive) {
deriveMissingBlockchains(scanResponse = scanResponse, currencyList = listOf(currency)) {
submitAdd(scanResponse = it, currency = currency)
}
} else {
submitAdd(scanResponse, currency)
}
}
private fun submitAdd(scanResponse: ScanResponse, currency: Currency) {
private fun isNeedToDerive(scanResponse: ScanResponse, currency: Currency): Boolean {
return currency.derivationPath?.let { !scanResponse.hasDerivation(currency.blockchain, it) } ?: false
}
private suspend fun deriveMissingBlockchains(
scanResponse: ScanResponse,
currencyList: List<Currency>,
onSuccess: suspend (ScanResponse) -> Unit,
) {
val derivationDataList = listOfNotNull(
getDerivations(EllipticCurve.Secp256k1, scanResponse, currencyList),
getDerivations(EllipticCurve.Ed25519, scanResponse, currencyList),
)
val derivations = derivationDataList.associate(TokensMiddleware.DerivationData::derivations)
if (derivations.isEmpty()) {
onSuccess(scanResponse)
return
}
when (val result = tangemSdkManager.derivePublicKeys(cardId = null, derivations = derivations)) {
is CompletionResult.Success -> {
val newDerivedKeys = result.data.entries
val oldDerivedKeys = scanResponse.derivedKeys
val walletKeys = (newDerivedKeys.keys + oldDerivedKeys.keys).toSet()
val updatedDerivedKeys = walletKeys.associateWith { walletKey ->
val oldDerivations = ExtendedPublicKeysMap(oldDerivedKeys[walletKey] ?: emptyMap())
val newDerivations = newDerivedKeys[walletKey] ?: ExtendedPublicKeysMap(emptyMap())
ExtendedPublicKeysMap(oldDerivations + newDerivations)
}
val updatedScanResponse = scanResponse.copy(derivedKeys = updatedDerivedKeys)
store.dispatchOnMain(GlobalAction.SaveScanResponse(updatedScanResponse))
delay(DELAY_SDK_DIALOG_CLOSE)
onSuccess(updatedScanResponse)
}
is CompletionResult.Failure -> {
store.dispatchDebugErrorNotification(TapError.CustomError("Error adding tokens"))
}
}
}
private fun getDerivations(
curve: EllipticCurve,
scanResponse: ScanResponse,
currencyList: List<Currency>,
): TokensMiddleware.DerivationData? {
val wallet = scanResponse.card.wallets.firstOrNull { it.curve == curve } ?: return null
val manageTokensCandidates = currencyList.map { it.blockchain }.distinct().filter {
it.getSupportedCurves().contains(curve)
}.mapNotNull {
it.derivationPath(scanResponse.card.derivationStyle)
}
val customTokensCandidates = currencyList.filter {
it.blockchain.getSupportedCurves().contains(curve)
}.mapNotNull { it.derivationPath }.map { DerivationPath(it) }
val bothCandidates = (manageTokensCandidates + customTokensCandidates).distinct()
if (bothCandidates.isEmpty()) return null
val mapKeyOfWalletPublicKey = wallet.publicKey.toMapKey()
val alreadyDerivedKeys: ExtendedPublicKeysMap =
scanResponse.derivedKeys[mapKeyOfWalletPublicKey] ?: ExtendedPublicKeysMap(emptyMap())
val alreadyDerivedPaths = alreadyDerivedKeys.keys.toList()
val toDerive = bothCandidates.filterNot { alreadyDerivedPaths.contains(it) }
if (toDerive.isEmpty()) return null
return TokensMiddleware.DerivationData(derivations = mapKeyOfWalletPublicKey to toDerive)
}
private suspend 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),
)
}
}
userWalletsListManager.update(
userWalletId = selectedUserWallet.walletId,
update = { userWallet -> userWallet.copy(scanResponse = scanResponse) },
)
.flatMap { updatedUserWallet ->
walletCurrenciesManager.addCurrencies(
userWallet = updatedUserWallet,
currenciesToAdd = listOf(currency),
)
}
}
}

View file

@ -3,14 +3,21 @@ package com.tangem.tap.features.customtoken.impl.domain.models
/**
* Found token model
*
* @property id id
* @property name name
* @property symbol symbol
* @property network network
* @property id id
* @property name name
* @property symbol symbol
* @property isActive flag that determines status of token
* @property network network
*
[REDACTED_AUTHOR]
*/
data class FoundToken(val id: String, val name: String, val symbol: String, val network: Network) {
data class FoundToken(
val id: String,
val name: String,
val symbol: String,
val isActive: Boolean,
val network: Network,
) {
/**
* Found token network

View file

@ -6,6 +6,7 @@ import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.transition.TransitionInflater
@ -24,6 +25,8 @@ import dagger.hilt.android.AndroidEntryPoint
internal class AddCustomTokenFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
activity?.window?.let { WindowCompat.setDecorFitsSystemWindows(it, true) }
with(TransitionInflater.from(requireContext())) {
enterTransition = inflateTransition(R.transition.fade)
exitTransition = inflateTransition(R.transition.fade)

View file

@ -1,8 +1,6 @@
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
@ -94,106 +92,94 @@ internal sealed interface AddCustomTokenInputField {
/** 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
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property isLoading flag that determine the processing of current value
* @property isError flag that determine if current value has error
* @property error error description
*/
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")
}
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val isLoading: Boolean,
val isError: Boolean,
val error: TextReference? = null,
) : AddCustomTokenInputField
/**
* 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
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property isEnabled input availability
*/
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
}
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val isEnabled: Boolean,
) : AddCustomTokenInputField
/**
* 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
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property isEnabled input availability
*/
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
}
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val isEnabled: Boolean,
) : AddCustomTokenInputField
/**
* 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
* @property value current value
* @property onValueChange lambda be invoked when value is been changed
* @property keyboardOptions keyboard options
* @property label label
* @property placeholder placeholder (hint)
* @property isEnabled input availability
*/
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
}
override val keyboardOptions: KeyboardOptions,
override val label: TextReference,
override val placeholder: TextReference,
val isEnabled: Boolean,
) : AddCustomTokenInputField
}
/** Base selector field model of add custom token screen */
internal sealed interface AddCustomTokenSelectorField {
/** Selection availability */
val isEnabled: Boolean
/** Label string resource id */
/** Label */
val label: TextReference
/** Selected menu item */
@ -208,35 +194,34 @@ internal sealed interface AddCustomTokenSelectorField {
/**
* Network selector model
*
* @property label label
* @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 label: TextReference,
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)
}
) : AddCustomTokenSelectorField
/**
* Derivation path selector model
*
* @property isEnabled selection availability
* @property label label
* @property selectedItem selected menu item
* @property items menu items
* @property onMenuItemClick lambda be invoked when menu item is been selected
* @property isEnabled selection availability
*/
data class DerivationPath(
override val isEnabled: Boolean,
override val label: TextReference,
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)
}
val isEnabled: Boolean,
) : AddCustomTokenSelectorField
/** Base menu item model */
sealed interface SelectorItem {
@ -259,17 +244,40 @@ internal sealed interface AddCustomTokenSelectorField {
* Menu item with title ans subtitle
*
* @property title title text
* @property subtitle subtitle text
* @property blockchain blockchain
* @property subtitle subtitle text
*/
data class TitleWithSubtitle(
override val title: TextReference,
val subtitle: TextReference,
override val blockchain: Blockchain,
val subtitle: TextReference,
) : SelectorItem
}
}
/**
* Warning model of add custom token screen
*
* @property description warning description
*/
internal sealed class AddCustomTokenWarning(val description: TextReference) {
/** Potential scam warning */
object PotentialScamToken : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_validation_error_not_found),
)
/** Token already added warning */
object TokenAlreadyAdded : AddCustomTokenWarning(
description = TextReference.Res(R.string.custom_token_validation_error_already_added),
)
/** Unsupported Solana token warning */
object UnsupportedSolanaToken : AddCustomTokenWarning(
description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message),
)
}
/**
* Floating button of add custom token screen
*

View file

@ -0,0 +1,4 @@
package com.tangem.tap.features.customtoken.impl.presentation.models
/** Custom token type */
enum class CustomTokenType { TOKEN, BLOCKCHAIN }

View file

@ -9,4 +9,7 @@ internal interface CustomTokenRouter {
/** Return to last screen */
fun popBackStack()
/** Open wallet (main) screen */
fun openWalletScreen()
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.customtoken.impl.presentation.routers
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.store
@ -9,4 +10,8 @@ internal class DefaultCustomTokenRouter : CustomTokenRouter {
override fun popBackStack() {
store.dispatch(NavigationAction.PopBackTo())
}
override fun openWalletScreen() {
store.dispatch(NavigationAction.PopBackTo(screen = AppScreen.Wallet))
}
}

View file

@ -4,8 +4,8 @@ import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTok
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.AddCustomTokenWarning
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
@ -24,7 +24,7 @@ internal sealed interface AddCustomTokenStateHolder {
val form: AddCustomTokenForm
/** Warnings */
val warnings: List<TextReference>
val warnings: Set<AddCustomTokenWarning>
/** Floating button model */
val floatingButton: AddCustomTokenFloatingButton
@ -42,7 +42,7 @@ internal sealed interface AddCustomTokenStateHolder {
onBackButtonClick: () -> Unit = this.onBackButtonClick,
toolbar: AddCustomTokensToolbar = this.toolbar,
form: AddCustomTokenForm = this.form,
warnings: List<TextReference> = this.warnings,
warnings: Set<AddCustomTokenWarning> = this.warnings,
floatingButton: AddCustomTokenFloatingButton = this.floatingButton,
): AddCustomTokenStateHolder {
return when (this) {
@ -64,7 +64,7 @@ internal sealed interface AddCustomTokenStateHolder {
override val onBackButtonClick: () -> Unit,
override val toolbar: AddCustomTokensToolbar,
override val form: AddCustomTokenForm,
override val warnings: List<TextReference>,
override val warnings: Set<AddCustomTokenWarning>,
override val floatingButton: AddCustomTokenFloatingButton,
) : AddCustomTokenStateHolder
@ -83,7 +83,7 @@ internal sealed interface AddCustomTokenStateHolder {
override val onBackButtonClick: () -> Unit,
override val toolbar: AddCustomTokensToolbar,
override val form: AddCustomTokenForm,
override val warnings: List<TextReference>,
override val warnings: Set<AddCustomTokenWarning>,
override val floatingButton: AddCustomTokenFloatingButton,
val testBlock: AddCustomTokenTestBlock,
val bottomSheet: AddCustomTokenChooseTokenBottomSheet,

View file

@ -9,22 +9,21 @@ 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.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.blockchain.common.Blockchain
import androidx.compose.ui.unit.dp
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
import com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarnings
/**
* Add custom token content
@ -37,6 +36,7 @@ import com.tangem.wallet.R
internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content) {
BackHandler(onBack = state.onBackButtonClick)
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
Scaffold(
topBar = {
AddCustomTokenToolbar(
@ -44,78 +44,36 @@ internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content) {
onBackButtonClick = state.toolbar.onBackButtonClick,
)
},
floatingActionButton = { AddCustomTokenFloatingButton(model = state.floatingButton) },
floatingActionButton = {
val density = LocalDensity.current
val verticalPadding = TangemTheme.dimens.spacing32
AddCustomTokenFloatingButton(
model = state.floatingButton,
modifier = Modifier.onSizeChanged {
floatingButtonHeight = with(density) { it.height.toDp() + verticalPadding }
},
)
},
floatingActionButtonPosition = FabPosition.Center,
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(paddingValues = it)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
.padding(bottom = floatingButtonHeight)
.fillMaxSize(),
) {
AddCustomTokenForm(model = state.form)
state.warnings.forEach { description ->
key(description) {
AddCustomTokenWarning(description)
}
}
AddCustomTokenWarnings(warnings = state.warnings)
}
}
}
@Preview(showSystemUi = true)
@Preview
@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 = {},
),
),
)
AddCustomTokenContent(state = AddCustomTokenPreviewData.createContent())
}
}

View file

@ -0,0 +1,124 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
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.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.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.AddCustomTokenWarning
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.details.ui.cardsettings.TextReference
import com.tangem.wallet.R
/**
[REDACTED_AUTHOR]
*/
internal object AddCustomTokenPreviewData {
fun createWarnings(): Set<AddCustomTokenWarning> {
return setOf(
AddCustomTokenWarning.PotentialScamToken,
AddCustomTokenWarning.TokenAlreadyAdded,
AddCustomTokenWarning.UnsupportedSolanaToken,
)
}
fun createDefaultForm(): AddCustomTokenForm {
return AddCustomTokenForm(
contractAddressInputField = AddCustomTokenInputField.ContactAddress(
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_contract_address_input_title),
placeholder = TextReference.Str(value = "0x0000000000000000000000000000000000000000"),
isLoading = false,
isError = false,
error = null,
),
networkSelectorField = AddCustomTokenSelectorField.Network(
label = TextReference.Res(R.string.custom_token_network_input_title),
selectedItem = AddCustomTokenSelectorField.SelectorItem.Title(
title = TextReference.Res(R.string.custom_token_network_input_not_selected),
blockchain = Blockchain.Unknown,
),
items = emptyList(),
onMenuItemClick = {},
),
tokenNameInputField = AddCustomTokenInputField.TokenName(
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_name_input_title),
placeholder = TextReference.Res(id = R.string.custom_token_name_input_placeholder),
isEnabled = false,
),
tokenSymbolInputField = AddCustomTokenInputField.TokenSymbol(
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_token_symbol_input_title),
placeholder = TextReference.Res(id = R.string.custom_token_token_symbol_input_placeholder),
isEnabled = false,
),
decimalsInputField = AddCustomTokenInputField.Decimals(
value = "",
onValueChange = {},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number, imeAction = ImeAction.Next),
label = TextReference.Res(R.string.custom_token_decimals_input_title),
placeholder = TextReference.Str(value = "8"),
isEnabled = false,
),
derivationPathSelectorField = AddCustomTokenSelectorField.DerivationPath(
label = TextReference.Res(R.string.custom_token_derivation_path_input_title),
selectedItem = 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,
),
items = emptyList(),
onMenuItemClick = {},
isEnabled = true,
),
)
}
fun createTestContent(): AddCustomTokenStateHolder.TestContent {
return AddCustomTokenStateHolder.TestContent(
onBackButtonClick = {},
toolbar = AddCustomTokensToolbar(
title = TextReference.Res(R.string.add_custom_token_title),
onBackButtonClick = {},
),
form = createDefaultForm(),
warnings = createWarnings(),
floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}),
testBlock = AddCustomTokenTestBlock(
chooseTokenButtonText = "Choose token",
clearButtonText = "Clear address",
resetButtonText = "Reset",
onClearAddressButtonClick = {},
onResetButtonClick = {},
),
bottomSheet = AddCustomTokenChooseTokenBottomSheet(categoriesBlocks = emptyList(), onTestTokenClick = {}),
)
}
fun createContent(): AddCustomTokenStateHolder.Content {
return AddCustomTokenStateHolder.Content(
onBackButtonClick = {},
toolbar = AddCustomTokensToolbar(
title = TextReference.Res(R.string.add_custom_token_title),
onBackButtonClick = {},
),
form = createDefaultForm(),
warnings = createWarnings(),
floatingButton = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}),
)
}
}

View file

@ -1,6 +1,10 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
/**
@ -10,11 +14,27 @@ import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTok
*
[REDACTED_AUTHOR]
*/
@Suppress("UnusedPrivateMember")
@Composable
internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder) {
when (stateHolder) {
is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(state = stateHolder)
is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(state = stateHolder)
}
}
}
@Preview(showSystemUi = true)
@Composable
private fun Preview_AddCustomTokenScreen(
@PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder,
) {
TangemTheme {
AddCustomTokenScreen(stateHolder)
}
}
private class AddCustomTokenScreenProvider : CollectionPreviewParameterProvider<AddCustomTokenStateHolder>(
collection = listOf(
AddCustomTokenPreviewData.createContent(),
AddCustomTokenPreviewData.createTestContent(),
),
)

View file

@ -4,6 +4,7 @@ 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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
@ -19,33 +20,33 @@ import androidx.compose.material.FabPosition
import androidx.compose.material.Text
import androidx.compose.material.rememberBottomSheetScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
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 com.tangem.tap.features.customtoken.impl.presentation.ui.components.AddCustomTokenWarnings
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
@ -74,6 +75,7 @@ internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestCont
},
)
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
BottomSheetScaffold(
sheetContent = {
SheetContent(
@ -95,7 +97,16 @@ internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestCont
},
)
},
floatingActionButton = { AddCustomTokenFloatingButton(model = state.floatingButton) },
floatingActionButton = {
val density = LocalDensity.current
val verticalPadding = TangemTheme.dimens.spacing32
AddCustomTokenFloatingButton(
model = state.floatingButton,
modifier = Modifier.onSizeChanged {
floatingButtonHeight = with(density) { it.height.toDp() + verticalPadding }
},
)
},
floatingActionButtonPosition = FabPosition.Center,
sheetBackgroundColor = TangemTheme.colors.background.secondary,
sheetPeekHeight = TangemTheme.dimens.size0,
@ -104,15 +115,19 @@ internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestCont
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(it),
.padding(paddingValues = it)
.padding(bottom = floatingButtonHeight)
.fillMaxSize(),
) {
TestBlock(
model = state.testBlock,
state.testBlock,
coroutineScope,
bottomSheetScaffoldState,
)
AddCustomTokenForm(model = state.form)
AddCustomTokenWarnings(warnings = state.warnings)
}
}
}
@ -243,69 +258,10 @@ private fun TestBlock(
}
}
@Preview(showSystemUi = true)
@Preview
@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 = {},
),
),
)
AddCustomTokenTestContent(state = AddCustomTokenPreviewData.createTestContent())
}
}

View file

@ -8,6 +8,8 @@ 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 androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.PrimaryButtonIconLeft
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
@ -16,14 +18,15 @@ import com.tangem.wallet.R
/**
* Add custom token floating button. Attached above the keyboard.
*
* @param model button model
* @param model button model
* @param modifier modifier
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton) {
internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, modifier: Modifier = Modifier) {
PrimaryButtonIconLeft(
modifier = Modifier
modifier = modifier
.imePadding()
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
@ -36,16 +39,17 @@ internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton) {
@Preview
@Composable
private fun Preview_AddCustomTokenFloatingButton_Enabled() {
private fun Preview_AddCustomTokenFloatingButton(
@PreviewParameter(AddCustomTokenFloatingButtonProvider::class) model: AddCustomTokenFloatingButton,
) {
TangemTheme {
AddCustomTokenFloatingButton(model = AddCustomTokenFloatingButton(isEnabled = true, onClick = {}))
AddCustomTokenFloatingButton(model)
}
}
@Preview
@Composable
private fun Preview_AddCustomTokenFloatingButton_Disabled() {
TangemTheme {
AddCustomTokenFloatingButton(model = AddCustomTokenFloatingButton(isEnabled = false, onClick = {}))
}
}
private class AddCustomTokenFloatingButtonProvider : CollectionPreviewParameterProvider<AddCustomTokenFloatingButton>(
listOf(
AddCustomTokenFloatingButton(isEnabled = true, onClick = {}),
AddCustomTokenFloatingButton(isEnabled = false, onClick = {}),
),
)

View file

@ -1,6 +1,10 @@
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -14,6 +18,7 @@ import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ExposedDropdownMenuBox
import androidx.compose.material.ExposedDropdownMenuDefaults
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -26,13 +31,14 @@ 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 androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
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.customtoken.impl.presentation.ui.AddCustomTokenPreviewData
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
/**
@ -68,7 +74,37 @@ internal fun AddCustomTokenForm(model: AddCustomTokenForm) {
@Composable
private fun InputField(model: AddCustomTokenInputField) {
Column {
val isError = (model as? AddCustomTokenInputField.ContactAddress)?.isError ?: false
TextField(model, isError)
(model as? AddCustomTokenInputField.ContactAddress)?.error?.resolveReference()?.let {
AnimatedVisibility(
visible = isError,
enter = fadeIn() + slideInVertically(),
exit = slideOutVertically() + fadeOut(),
) {
Text(
text = it,
color = MaterialTheme.colors.error,
style = TangemTheme.typography.body2,
)
}
}
}
}
@Composable
private fun TextField(model: AddCustomTokenInputField, isError: Boolean) {
Box {
val isEnabled = when (model) {
is AddCustomTokenInputField.ContactAddress -> true
is AddCustomTokenInputField.Decimals -> model.isEnabled
is AddCustomTokenInputField.TokenName -> model.isEnabled
is AddCustomTokenInputField.TokenSymbol -> model.isEnabled
}
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = model.value,
@ -79,8 +115,8 @@ private fun InputField(model: AddCustomTokenInputField) {
text = model.label.resolveReference(),
style = TangemTheme.typography.caption,
color = TangemTextFieldsDefault.defaultTextFieldColors.labelColor(
enabled = model.isEnabled,
error = model.isError,
enabled = isEnabled,
error = isError,
interactionSource = remember { MutableInteractionSource() },
).value,
)
@ -90,20 +126,20 @@ private fun InputField(model: AddCustomTokenInputField) {
text = model.placeholder.resolveReference(),
style = TangemTheme.typography.body1,
color = TangemTextFieldsDefault.defaultTextFieldColors
.placeholderColor(enabled = model.isEnabled)
.placeholderColor(enabled = isEnabled)
.value,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
singleLine = true,
enabled = model.isEnabled,
isError = model.isError,
enabled = isEnabled,
isError = isError,
colors = TangemTextFieldsDefault.defaultTextFieldColors,
)
AnimatedVisibility(
visible = model.isLoading,
visible = (model as? AddCustomTokenInputField.ContactAddress)?.isLoading ?: false,
modifier = Modifier
.fillMaxWidth()
.align(Alignment.BottomCenter)
@ -124,6 +160,7 @@ private fun SelectorField(model: AddCustomTokenSelectorField) {
expanded = isExpanded,
onExpandedChange = { isExpanded = !isExpanded },
) {
val isEnabled = (model as? AddCustomTokenSelectorField.DerivationPath)?.isEnabled ?: true
OutlinedTextField(
value = when (val item = model.selectedItem) {
is AddCustomTokenSelectorField.SelectorItem.Title -> item.title
@ -132,14 +169,14 @@ private fun SelectorField(model: AddCustomTokenSelectorField) {
modifier = Modifier.fillMaxWidth(),
onValueChange = {},
readOnly = true,
enabled = model.isEnabled,
enabled = isEnabled,
label = { Text(text = model.label.resolveReference()) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
colors = TangemTextFieldsDefault.defaultTextFieldColors,
)
ExposedDropdownMenu(
expanded = isExpanded && model.isEnabled,
expanded = isExpanded && isEnabled,
onDismissRequest = { isExpanded = false },
) {
FocusRequester
@ -173,44 +210,18 @@ private fun SelectorField(model: AddCustomTokenSelectorField) {
@Preview
@Composable
private fun Preview_AddCustomTokenForm() {
private fun Preview_AddCustomTokenForm(@PreviewParameter(AddCustomTokenFormProvider::class) model: 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,
),
)
AddCustomTokenForm(model)
}
}
}
private class AddCustomTokenFormProvider : CollectionPreviewParameterProvider<AddCustomTokenForm>(
collection = listOf(
AddCustomTokenPreviewData.createDefaultForm(),
AddCustomTokenPreviewData.createDefaultForm().copy(derivationPathSelectorField = null),
AddCustomTokenPreviewData.createDefaultForm().let { form ->
form.copy(contractAddressInputField = form.contractAddressInputField.copy(isLoading = true))
},
),
)

View file

@ -2,39 +2,56 @@ 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.fillMaxSize
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
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.customtoken.impl.presentation.models.AddCustomTokenWarning
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenPreviewData
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")
* Add custom token warnings
*
* @param description warning description
* @param modifier modifier
* @param warnings warnings descriptions set
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun AddCustomTokenWarning(description: TextReference, modifier: Modifier = Modifier) {
internal fun AddCustomTokenWarnings(warnings: Set<AddCustomTokenWarning>) {
Column(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
warnings.forEach { warning ->
key(warning) { AddCustomTokenWarning(warning) }
}
}
}
@Composable
private fun AddCustomTokenWarning(warning: AddCustomTokenWarning) {
Card(
modifier = modifier,
shape = RoundedCornerShape(TangemTheme.dimens.radius4),
modifier = Modifier.fillMaxSize(),
shape = TangemTheme.shapes.roundedCornersSmall2,
backgroundColor = TangemColorPalette.Tangerine,
contentColor = TangemColorPalette.White,
elevation = TangemTheme.dimens.elevation4,
) {
// FIXME("Incorrect typography. Replace with typography from design system")
Column(
modifier = Modifier.padding(all = TangemTheme.dimens.spacing16),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
@ -45,10 +62,18 @@ internal fun AddCustomTokenWarning(description: TextReference, modifier: Modifie
style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
)
Text(
text = description.resolveReference(),
text = warning.description.resolveReference(),
fontSize = 13.sp,
lineHeight = 18.sp,
)
}
}
}
@Preview
@Composable
private fun Preview_AddCustomTokenWarnings() {
TangemTheme {
AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings())
}
}

View file

@ -0,0 +1,17 @@
package com.tangem.tap.features.customtoken.impl.presentation.viewmodels
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.analytics.events.ManageTokens
/** Analytics sender for tokens list screen */
class AddCustomTokenAnalyticsSender(private val analyticsEventHandler: AnalyticsEventHandler) {
fun sendWhenScreenOpened() {
analyticsEventHandler.send(ManageTokens.CustomToken.ScreenOpened)
}
fun sendWhenAddTokenButtonClicked(customCurrency: CustomCurrency) {
analyticsEventHandler.send(ManageTokens.CustomToken.TokenWasAdded(customCurrency))
}
}

View file

@ -15,11 +15,11 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.analytics.events.ManageTokens
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.customtoken.legacy.compose.AddCustomTokenScreen
import com.tangem.tap.features.customtoken.legacy.compose.ClosePopupTrigger
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber

View file

@ -49,8 +49,6 @@ import com.tangem.domain.features.addCustomToken.redux.ScreenState
import com.tangem.domain.features.addCustomToken.redux.ViewStates
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.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.customtoken.legacy.compose.test.TestCase
import com.tangem.tap.features.customtoken.legacy.compose.test.TestCasesList
@ -174,12 +172,14 @@ fun Warnings(warnings: List<AddCustomTokenError.Warning>) {
Column {
warnings.forEachIndexed { index, item ->
val modifier = when (index) {
0 -> Modifier.padding(16.dp, 0.dp, 16.dp, 0.dp)
warnings.lastIndex -> Modifier.padding(16.dp, 8.dp, 16.dp, 16.dp)
else -> Modifier.padding(16.dp, 8.dp, 16.dp, 0.dp)
0 -> Modifier.padding(vertical = 0.dp)
warnings.lastIndex -> Modifier.padding(top = 8.dp, bottom = 16.dp)
else -> Modifier.padding(top = 8.dp, bottom = 0.dp)
}
AddCustomTokenWarning(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.padding(horizontal = TangemTheme.dimens.spacing16)
.fillMaxWidth(),
warning = item,
converter = warningConverter,
)

View file

@ -1,4 +1,4 @@
package com.tangem.tap.common.compose
package com.tangem.tap.features.customtoken.legacy.compose
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.fillMaxWidth

View file

@ -1,4 +1,4 @@
package com.tangem.tap.common.compose
package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
@ -31,17 +31,18 @@ import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.res.TangemTheme
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
import com.tangem.domain.DomainDialog
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.customtoken.legacy.compose.SelectTokenNetworkDialog
import com.tangem.wallet.R
import org.rekotlin.StoreSubscriber
@Composable
fun ComposeDialogManager() {
internal fun ComposeDialogManager() {
val dialogSate = remember { mutableStateOf<DomainDialog?>(null) }
val subscriber = remember {
object : StoreSubscriber<DomainGlobalState> {
@ -94,27 +95,22 @@ private fun ShowTheDialog(dialogState: MutableState<DomainDialog?>) {
* Dialog with single item selection
*/
@Composable
fun <T> SimpleDialog(
fun SimpleDialog(
title: String,
items: List<T>,
onSelect: (T) -> Unit,
items: List<CoinsResponse.Coin.Network>,
onSelect: (CoinsResponse.Coin.Network) -> Unit,
onDismissRequest: () -> Unit,
itemContent: @Composable (T) -> Unit,
itemContent: @Composable (CoinsResponse.Coin.Network) -> Unit,
) {
Dialog(
properties = DialogProperties(false, false),
properties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
onDismissRequest = { },
) {
Surface(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
) {
Column(
modifier = Modifier.padding(22.dp),
) {
Surface(modifier = Modifier.fillMaxWidth(), shape = MaterialTheme.shapes.medium) {
Column(modifier = Modifier.padding(TangemTheme.dimens.spacing22)) {
DialogTitle(title = title)
LazyColumn {
items(items) { item ->
items(items = items, key = CoinsResponse.Coin.Network::networkId) { item ->
Row(
modifier = Modifier
.fillMaxWidth()

View file

@ -17,8 +17,6 @@ import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTo
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenAction.OnTokenSymbolChanged
import com.tangem.domain.features.addCustomToken.redux.AddCustomTokenState
import com.tangem.domain.redux.domainStore
import com.tangem.tap.common.compose.BlockchainSpinner
import com.tangem.tap.common.compose.ClosePopupTrigger
import com.tangem.tap.common.compose.OutlinedTextFieldWidget
import com.tangem.tap.common.compose.TitleSubtitle
import com.tangem.wallet.R

View file

@ -1,9 +1,10 @@
package com.tangem.tap.common.compose
package com.tangem.tap.features.customtoken.legacy.compose
import android.os.Handler
import android.os.Looper
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -12,6 +13,7 @@ import androidx.core.os.postDelayed
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.domain.common.form.Field
import com.tangem.tap.common.compose.TangemTextFieldsDefault
import com.tangem.tap.common.extensions.ValueCallback
/**
@ -20,7 +22,7 @@ import com.tangem.tap.common.extensions.ValueCallback
@Suppress("MagicNumber")
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun <T> OutlinedSpinner(
internal fun <T> OutlinedSpinner(
label: String,
itemList: List<T>,
selectedItem: Field.Data<T>,
@ -71,15 +73,11 @@ fun <T> OutlinedSpinner(
)
if (isEnabled) {
ExposedDropdownMenu(
expanded = rIsExpanded.value,
onDismissRequest = onDismissRequest,
) {
ExposedDropdownMenu(expanded = rIsExpanded.value, onDismissRequest = onDismissRequest) {
itemList.forEach { item ->
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
when (dropdownItemView) {
null -> Text(textFieldConverter(item))
else -> dropdownItemView(item)
key(item) {
DropdownMenuItem(onClick = { onDropDownItemSelectedInternal(item) }) {
if (dropdownItemView == null) Text(textFieldConverter(item)) else dropdownItemView(item)
}
}
}
@ -91,7 +89,6 @@ fun <T> OutlinedSpinner(
class ClosePopupTrigger {
var close: () -> Unit = {}
var onCloseComplete: () -> Unit = {}
var isTriggered = false
}
@Preview

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.customtoken.legacy.compose
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import com.tangem.domain.DomainDialog
import com.tangem.tap.common.compose.SimpleDialog
import com.tangem.tap.common.compose.TitleSubtitle
import com.tangem.wallet.R

View file

@ -24,7 +24,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -86,13 +85,13 @@ fun CardSettingsReadCard(onScanCardClick: () -> Unit) {
Text(
text = stringResource(id = R.string.scan_card_settings_title),
color = colorResource(id = R.color.text_primary_1),
style = TangemTypography.headline3,
style = TangemTheme.typography.h3,
)
Spacer(modifier = Modifier.size(20.dp))
Text(
text = stringResource(id = R.string.scan_card_settings_message),
color = colorResource(id = R.color.text_secondary),
style = TangemTypography.body1,
style = TangemTheme.typography.body1,
modifier = Modifier
.verticalScroll(rememberScrollState())
.weight(weight = 1f, fill = false),

View file

@ -67,6 +67,7 @@ sealed class CardInfo(
)
}
// TODO("Remove and use the same from coreUI")
sealed interface TextReference {
class Res(@StringRes val id: Int, val formatArgs: List<Any> = emptyList()) : TextReference {
constructor(@StringRes id: Int, vararg formatArgs: Any) : this(id, formatArgs.toList())

View file

@ -27,7 +27,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButtonIconRight
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.wallet.R
@Composable
@ -145,13 +144,13 @@ fun DetailsRadioButtonElement(title: String, subtitle: String, selected: Boolean
Column {
Text(
text = title,
style = TangemTypography.subtitle1,
style = TangemTheme.typography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
Spacer(modifier = Modifier.size(4.dp))
Text(
text = subtitle,
style = TangemTypography.body2,
style = TangemTheme.typography.body2,
color = colorResource(id = R.color.text_secondary),
)
}

View file

@ -35,7 +35,6 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SystemBarsEffect
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -78,7 +77,7 @@ fun Content(state: DetailsScreenState) {
Spacer(modifier = Modifier.size(12.dp))
Text(
text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}",
style = TangemTypography.caption,
style = TangemTheme.typography.caption,
color = colorResource(id = R.color.text_tertiary),
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp),
)
@ -111,13 +110,13 @@ fun WalletConnectDetailsItem(onItemsClick: (SettingsElement) -> Unit) {
Text(
text = stringResource(id = R.string.wallet_connect_title),
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTypography.headline3,
style = TangemTheme.typography.h3,
color = colorResource(id = R.color.text_primary_1),
)
Text(
text = stringResource(id = R.string.wallet_connect_subtitle),
modifier = Modifier.padding(end = 20.dp, bottom = 4.dp),
style = TangemTypography.body1,
style = TangemTheme.typography.body1,
color = colorResource(id = R.color.text_secondary),
)
}
@ -144,13 +143,13 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () ->
Text(
text = stringResource(id = item.titleRes),
modifier = Modifier,
style = TangemTypography.subtitle1,
style = TangemTheme.typography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
)
if (item == SettingsElement.AppCurrency) {
Text(
text = appCurrency,
style = TangemTypography.body2,
style = TangemTheme.typography.body2,
color = colorResource(id = R.color.text_secondary),
)
}

View file

@ -31,8 +31,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.analytics.Analytics
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.Settings
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.common.extensions.getFromClipboard
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -105,7 +105,7 @@ private fun EmptyScreen(state: WalletConnectScreenState) {
Spacer(modifier = Modifier.size(24.dp))
Text(
text = stringResource(id = R.string.wallet_connect_subtitle),
style = TangemTypography.body2,
style = TangemTheme.typography.body2,
color = colorResource(id = R.color.text_tertiary),
)
}
@ -138,7 +138,7 @@ private fun WalletConnectSessions(state: WalletConnectScreenState) {
) {
Text(
text = session.description,
style = TangemTypography.subtitle1,
style = TangemTheme.typography.subtitle1,
color = colorResource(id = R.color.text_primary_1),
modifier = Modifier.weight(1f),
)

View file

@ -1,6 +1,6 @@
package com.tangem.tap.features.disclaimer
import com.tangem.tap.persistence.DisclaimerPrefStorage
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
/**
[REDACTED_AUTHOR]

View file

@ -1,11 +1,11 @@
package com.tangem.tap.features.disclaimer
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.tap.persistence.DisclaimerPrefStorage
import com.tangem.domain.models.scan.CardDTO
import com.tangem.data.source.preferences.storage.DisclaimerPrefStorage
import com.tangem.tap.preferencesStorage
import java.util.*
import java.util.Locale
/**
[REDACTED_AUTHOR]

View file

@ -91,8 +91,7 @@ class HomeFragment : Fragment(), StoreSubscriber<HomeState> {
onSearchTokensClick = {
Analytics.send(IntroductionProcess.ButtonTokensList())
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
store.dispatch(TokensAction.AllowToAddTokens(false))
store.dispatch(TokensAction.LoadCurrencies())
store.dispatch(TokensAction.SetArgs.ReadAccess)
},
)
}

View file

@ -40,6 +40,14 @@ object OnboardingHelper {
cardInfoStorage.isActivationInProgress(cardId)
}
}
response.cardTypesResolver.isWallet2() -> {
val emptyWallets = response.card.wallets.isEmpty()
val activationInProgress = cardInfoStorage.isActivationInProgress(cardId)
val backupNotActive = response.card.backupStatus?.isActive != true
emptyWallets || activationInProgress || backupNotActive
}
response.card.wallets.isNotEmpty() -> cardInfoStorage.isActivationInProgress(cardId)
else -> true
}

View file

@ -15,7 +15,7 @@ import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.persistence.UsedCardsPrefStorage
import com.tangem.data.source.preferences.storage.UsedCardsPrefStorage
import timber.log.Timber
import java.math.BigDecimal

View file

@ -1,13 +1,22 @@
package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.tangem.common.CompletionResult
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import org.rekotlin.Action
sealed class OnboardingWalletAction : Action {
object Init : OnboardingWalletAction()
object GetToCreateWalletStep : OnboardingWalletAction()
object GetToSaltPayStep : OnboardingWalletAction()
object CreateWallet : OnboardingWalletAction()
data class WalletWasCreated(
val result: CompletionResult<CreateProductWalletTaskResponse>,
) : OnboardingWalletAction()
object Done : OnboardingWalletAction()
object FinishOnboarding : OnboardingWalletAction()
@ -19,6 +28,25 @@ sealed class OnboardingWalletAction : Action {
object OnBackPressed : OnboardingWalletAction()
}
sealed class OnboardingWallet2Action : OnboardingWalletAction() {
data class Init(val maxProgress: Int) : OnboardingWallet2Action()
data class SetDependencies(val maxProgress: Int) : OnboardingWallet2Action()
data class CreateWallet(
val callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) : OnboardingWallet2Action()
data class ImportWallet(
val mnemonicComponents: List<String>,
val seedPhraseSource: SeedPhraseSource,
val callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) : OnboardingWallet2Action()
data class WalletWasCreated(
val result: CompletionResult<CreateWalletResponse>,
) : OnboardingWallet2Action()
}
sealed class BackupAction : Action {
object IntroduceBackup : BackupAction()
@ -62,4 +90,6 @@ sealed class BackupAction : Action {
object DiscardBackup : BackupAction()
object DiscardSavedBackup : BackupAction()
object ResumeFoundUnfinishedBackup : BackupAction()
data class ResetBackupCard(val cardId: String) : BackupAction()
}

View file

@ -3,15 +3,19 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
import android.net.Uri
import com.tangem.blockchain.common.Blockchain
import com.tangem.common.CompletionResult
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.ifNotNull
import com.tangem.core.analytics.Analytics
import com.tangem.domain.common.TapWorkarounds.isSaltPay
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.operations.backup.BackupService
import com.tangem.tap.backupService
import com.tangem.tap.*
import com.tangem.tap.common.analytics.events.AnalyticsParam
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchOnMain
@ -20,6 +24,7 @@ import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.tasks.product.CreateProductWalletTaskResponse
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.home.redux.HomeAction
@ -29,12 +34,6 @@ import com.tangem.tap.features.onboarding.products.wallet.saltPay.SaltPayActivat
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.wallet.models.toCurrencies
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdk
import com.tangem.tap.tangemSdkManager
import com.tangem.tap.userTokensRepository
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -47,7 +46,10 @@ object OnboardingWalletMiddleware {
private val onboardingWalletMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
handleWalletAction(action, state, dispatch)
when (action) {
is OnboardingWallet2Action -> handleWallet2Action(action, state)
else -> handleWalletAction(action, state, dispatch)
}
next(action)
}
}
@ -114,42 +116,44 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
}
}
is OnboardingWalletAction.CreateWallet -> {
scanResponse ?: return
scope.launch {
scanResponse ?: return@launch
val result = tangemSdkManager.createProductWallet(scanResponse)
withMainContext {
when (result) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
// here we must use updated scanResponse after createWallet & derivation
val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
onboardingManager.scanResponse = updatedResponse
store.state.globalState.topUpController?.registerEmptyWallet(updatedResponse)
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(result))
}
}
is OnboardingWalletAction.WalletWasCreated -> {
scanResponse ?: return
when (val result = action.result) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
// here we must use updated scanResponse after createWallet & derivation
val updatedResponse = globalState.onboardingState.onboardingManager.scanResponse.copy(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
onboardingManager.scanResponse = updatedResponse
store.state.globalState.topUpController?.registerEmptyWallet(updatedResponse)
val blockchainNetworks = if (DemoHelper.isDemoCardId(result.data.card.cardId)) {
DemoHelper.config.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}.map { blockchain ->
BlockchainNetwork(blockchain, result.data.card)
}
scope.launch {
userTokensRepository.saveUserTokens(
card = result.data.card,
tokens = blockchainNetworks.toCurrencies(),
)
}
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
is CompletionResult.Failure -> Unit
val blockchainNetworks = if (DemoHelper.isDemoCardId(result.data.card.cardId)) {
DemoHelper.config.demoBlockchains
} else {
listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
}.map { blockchain ->
BlockchainNetwork(blockchain, result.data.card)
}
scope.launch {
userTokensRepository.saveUserTokens(
card = result.data.card,
tokens = blockchainNetworks.toCurrencies(),
)
}
startCardActivation(updatedResponse)
store.dispatch(OnboardingWalletAction.ResumeBackup)
}
is CompletionResult.Failure -> Unit
}
}
OnboardingWalletAction.FinishOnboarding -> {
@ -194,6 +198,99 @@ private fun handleWalletAction(action: Action, state: () -> AppState?, dispatch:
}
}
@Suppress("LongMethod", "ComplexMethod")
private fun handleWallet2Action(action: OnboardingWallet2Action, state: () -> AppState?) {
val globalState = store.state.globalState
val onboardingManager = globalState.onboardingState.onboardingManager
val scanResponse = onboardingManager?.scanResponse
when (action) {
is OnboardingWallet2Action.Init -> {
if (scanResponse?.cardTypesResolver?.isWallet2() == true) {
store.dispatch(OnboardingWallet2Action.SetDependencies(action.maxProgress))
}
}
is OnboardingWallet2Action.CreateWallet -> {
scanResponse ?: return
scope.launch {
val mediateResult = when (val result = tangemSdkManager.createProductWallet(scanResponse)) {
is CompletionResult.Success -> {
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully())
val response = CreateWalletResponse(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
withMainContext {
action.callback(mediateResult)
}
}
}
is OnboardingWallet2Action.ImportWallet -> {
scanResponse ?: return
scope.launch {
val mediateResult = when (
val result = tangemSdkManager.importWallet(
scanResponse = scanResponse,
mnemonic = action.mnemonicComponents.joinToString(" "),
)
) {
is CompletionResult.Success -> {
val creationType = when (action.seedPhraseSource) {
SeedPhraseSource.IMPORTED -> AnalyticsParam.WalletCreationType.SeedImport
SeedPhraseSource.GENERATED -> AnalyticsParam.WalletCreationType.NewSeed
}
Analytics.send(Onboarding.CreateWallet.WalletCreatedSuccessfully(creationType))
val response = CreateWalletResponse(
card = result.data.card,
derivedKeys = result.data.derivedKeys,
primaryCard = result.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
withMainContext {
action.callback(mediateResult)
}
}
}
is OnboardingWallet2Action.WalletWasCreated -> {
val result = when (val mediateResult = action.result) {
is CompletionResult.Success -> {
val response = CreateProductWalletTaskResponse(
card = mediateResult.data.card,
derivedKeys = mediateResult.data.derivedKeys,
primaryCard = mediateResult.data.primaryCard,
)
CompletionResult.Success(response)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(mediateResult.error)
}
}
store.dispatchOnMain(OnboardingWalletAction.WalletWasCreated(result))
}
else -> Unit
}
}
private fun updateScanResponseAfterBackup(scanResponse: ScanResponse, backupState: BackupState): ScanResponse {
val card = if (backupState.backupCardsNumber > 0) {
val cardsCount = backupState.backupCardsNumber
@ -263,7 +360,19 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
is CompletionResult.Success -> {
store.dispatchOnMain(BackupAction.AddBackupCard.Success)
}
is CompletionResult.Failure -> Unit
is CompletionResult.Failure -> {
val error = result.error
if (error is TangemSdkError.BackupFailedNotEmptyWallets &&
onboardingWalletState.wallet2State != null
) {
store.dispatchOnMain(
GlobalAction.ShowDialog(
BackupDialog.ResetBackupCard(error.cardId),
),
)
}
}
}
}
}
@ -368,6 +477,11 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
Analytics.send(Onboarding.Finished())
finishCardActivation(notActivatedCardIds)
}
is BackupAction.ResetBackupCard -> {
scope.launch { tangemSdkManager.resetToFactorySettings(action.cardId) }
}
else -> Unit
}
}

View file

@ -19,8 +19,8 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state)
is OnboardingSaltPayAction -> OnboardingSaltPayReducer.reduce(action, state)
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState, state.isSaltPay))
is OnboardingWalletAction.GetToCreateWalletStep -> OnboardingWalletState(
is OnboardingWallet2Action -> OnboardingWallet2Reducer.reduce(action, state)
is OnboardingWalletAction.GetToCreateWalletStep -> state.copy(
step = OnboardingWalletStep.CreateWallet,
)
is OnboardingWalletAction.GetToSaltPayStep -> state.copy(
@ -65,6 +65,17 @@ private object ReducerForGlobalAction {
private fun maxBackupCards(isSaltPay: Boolean): Int = if (isSaltPay) 1 else 2
}
private object OnboardingWallet2Reducer {
fun reduce(action: OnboardingWallet2Action, state: OnboardingWalletState): OnboardingWalletState = when (action) {
is OnboardingWallet2Action.SetDependencies -> state.copy(
wallet2State = OnboardingWallet2State(
maxProgress = action.maxProgress,
),
)
else -> state
}
}
private object BackupReducer {
@Suppress("ComplexMethod")
fun reduce(action: BackupAction, state: BackupState, isSaltPay: Boolean): BackupState {

View file

@ -14,6 +14,7 @@ import org.rekotlin.StateType
*/
data class OnboardingWalletState(
val step: OnboardingWalletStep = OnboardingWalletStep.None,
val wallet2State: OnboardingWallet2State? = null,
val backupState: BackupState = BackupState(),
val onboardingSaltPayState: OnboardingSaltPayState? = null,
val isSaltPay: Boolean = false,
@ -31,14 +32,14 @@ data class OnboardingWalletState(
}
@Suppress("MagicNumber")
fun getMaxProgress(): Int = when {
isSaltPay -> 12
else -> 6
fun getMaxProgress(): Int {
val baseProgress = if (isSaltPay) 12 else 6
return getWallet2Progress() + baseProgress
}
@Suppress("ComplexMethod", "MagicNumber")
fun getProgressStep(): Int {
return when {
val progressByStep = when {
step == OnboardingWalletStep.CreateWallet -> 1
step == OnboardingWalletStep.Backup -> {
when (backupState.backupStep) {
@ -69,11 +70,19 @@ data class OnboardingWalletState(
step == OnboardingWalletStep.Done -> getMaxProgress()
else -> 1
}
return getWallet2Progress() + progressByStep
}
private fun getWallet2Progress(): Int = wallet2State?.maxProgress ?: 0
}
data class OnboardingWallet2State(
val maxProgress: Int,
)
enum class OnboardingWalletStep {
None, CreateWallet, Backup, SaltPay, Done
None, CreateWallet, SaltPay, Backup, Done
}
data class BackupState(
@ -113,4 +122,5 @@ sealed class BackupDialog : StateDialog {
object BackupInProgress : BackupDialog()
object UnfinishedBackupFound : BackupDialog()
object ConfirmDiscardingBackup : BackupDialog()
data class ResetBackupCard(val cardId: String) : BackupDialog()
}

View file

@ -18,15 +18,7 @@ import com.tangem.common.extensions.guard
import com.tangem.core.analytics.Analytics
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.compose.PinCodeWidget
import com.tangem.tap.common.extensions.configureSettings
import com.tangem.tap.common.extensions.dispatchDebugErrorNotification
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.getDrawableCompat
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.setDrawable
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.stop
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
@ -34,11 +26,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.toggleWidget.IndeterminateProgressButtonWidget
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebChromeClient
import com.tangem.tap.features.onboarding.products.wallet.saltPay.UtorgWebViewClient
import com.tangem.tap.features.onboarding.products.wallet.saltPay.dialog.SaltPayDialog
@ -56,7 +44,7 @@ import com.tangem.wallet.databinding.LayoutOnboardingMainBinding
/**
[REDACTED_AUTHOR]
*/
internal class OnboardingSaltPayView(
internal class OnboardingSaltPayStateHandler(
private val walletFragment: OnboardingWalletFragment,
) {

View file

@ -0,0 +1,54 @@
package com.tangem.tap.features.onboarding.products.wallet.ui
import androidx.compose.runtime.collectAsState
import com.tangem.feature.onboarding.api.OnboardingSeedPhrase
import com.tangem.feature.onboarding.api.OnboardingSeedPhraseApi
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
/**
[REDACTED_AUTHOR]
*/
internal class OnboardingSeedPhraseStateHandler(
private val onboardingSeedPhraseApi: OnboardingSeedPhraseApi = OnboardingSeedPhrase(),
) {
fun newState(
walletFragment: OnboardingWalletFragment,
state: OnboardingWalletState,
seedPhraseViewModel: SeedPhraseViewModel,
) {
if (state.step == OnboardingWalletStep.CreateWallet && !seedPhraseViewModel.isFinished) {
switchToWallet2SeedPhraseOnboarding(walletFragment, seedPhraseViewModel, state.getMaxProgress())
} else {
switchToWalletOnboarding(walletFragment, state)
}
}
private fun switchToWalletOnboarding(walletFragment: OnboardingWalletFragment, state: OnboardingWalletState) {
walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.hide()
walletFragment.pbBinding.pbState.show()
walletFragment.binding.onboardingWalletContainer.show()
walletFragment.handleOnboardingStep(state)
}
private fun switchToWallet2SeedPhraseOnboarding(
walletFragment: OnboardingWalletFragment,
viewModel: SeedPhraseViewModel,
onboardingWalletMaxProgress: Int,
) {
walletFragment.pbBinding.pbState.hide()
walletFragment.binding.onboardingWalletContainer.hide()
walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.show()
walletFragment.bindingSeedPhrase.onboardingSeedPhraseContainer.setContent {
onboardingSeedPhraseApi.ScreenContent(
uiState = viewModel.uiState,
subScreen = viewModel.currentScreen.collectAsState().value,
progress = viewModel.progress.collectAsState(0).value.toFloat() / onboardingWalletMaxProgress,
)
}
}
}

View file

@ -10,6 +10,7 @@ import android.widget.FrameLayout
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.MenuProvider
import androidx.fragment.app.viewModels
import androidx.transition.TransitionInflater
import androidx.transition.TransitionManager
import by.kirich1409.viewbindingdelegate.viewBinding
@ -17,46 +18,56 @@ import coil.load
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.tabs.TabLayoutMediator
import com.tangem.common.CardIdFormatter
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardIdDisplayFormat
import com.tangem.core.analytics.Analytics
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseMediator
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseRouter
import com.tangem.feature.onboarding.presentation.wallet2.viewmodel.SeedPhraseViewModel
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
import com.tangem.sdk.ui.widget.leapfrogWidget.PropertyCalculator
import com.tangem.tap.common.analytics.events.Onboarding
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.inflate
import com.tangem.tap.common.extensions.resourceUri
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.BaseFragment
import com.tangem.tap.features.FragmentOnBackPressedHandler
import com.tangem.tap.features.addBackPressHandler
import com.tangem.tap.features.onboarding.OnboardingMenuProvider
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupState
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupStep
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletState
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletStep
import com.tangem.tap.features.onboarding.products.wallet.redux.*
import com.tangem.tap.features.onboarding.products.wallet.saltPay.redux.OnboardingSaltPayAction
import com.tangem.tap.features.onboarding.products.wallet.saltPay.ui.OnboardingSaltPayView
import com.tangem.tap.features.onboarding.products.wallet.saltPay.ui.OnboardingSaltPayStateHandler
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentOnboardingWalletBinding
import com.tangem.wallet.databinding.LayoutOnboardingSaltpayBinding
import com.tangem.wallet.databinding.LayoutOnboardingSeedPhraseBinding
import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.rekotlin.StoreSubscriber
@Suppress("LargeClass", "MagicNumber")
@AndroidEntryPoint
class OnboardingWalletFragment :
BaseFragment(R.layout.fragment_onboarding_wallet),
StoreSubscriber<OnboardingWalletState>,
FragmentOnBackPressedHandler {
private val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
internal val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer }
internal val bindingSaltPay: LayoutOnboardingSaltpayBinding by lazy { binding.onboardingSaltpayContainer }
private val onboardingSaltPayView: OnboardingSaltPayView = OnboardingSaltPayView(this)
private val seedPhraseStateHandler: OnboardingSeedPhraseStateHandler = OnboardingSeedPhraseStateHandler()
private val seedPhraseViewModel by viewModels<SeedPhraseViewModel>()
private val saltPayStateHandler: OnboardingSaltPayStateHandler = OnboardingSaltPayStateHandler(this)
private lateinit var cardsWidget: WalletCardsWidget
private var accessCodeDialog: AccessCodeDialog? = null
@ -69,6 +80,13 @@ class OnboardingWalletFragment :
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
seedPhraseViewModel.setRouter(makeSeedPhraseRouter())
seedPhraseViewModel.setMediator(makeSeedPhraseMediator())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -88,6 +106,7 @@ class OnboardingWalletFragment :
binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() }
addBackPressHandler(this)
store.dispatch(OnboardingWallet2Action.Init(seedPhraseViewModel.maxProgress))
store.dispatch(OnboardingWalletAction.Init)
store.dispatch(OnboardingSaltPayAction.Init)
store.dispatch(
@ -97,7 +116,7 @@ class OnboardingWalletFragment :
)
}
override fun loadToolbarMenu(): MenuProvider? = OnboardingMenuProvider()
override fun loadToolbarMenu(): MenuProvider = OnboardingMenuProvider()
private fun reInitCardsWidgetIfNeeded(backupCardsCounts: Int) = with(binding) {
val viewBackupCount = flCardsContainer.childCount - 1
@ -159,13 +178,19 @@ class OnboardingWalletFragment :
pbBinding.pbState.max = state.getMaxProgress()
pbBinding.pbState.progress = state.getProgressStep()
if (state.isSaltPay) {
onboardingSaltPayView.newState(state)
} else {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvSecondBackupCard)
handleOnboardingStep(state)
when {
state.wallet2State != null -> {
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
}
state.isSaltPay -> {
saltPayStateHandler.newState(state)
}
else -> {
loadImageIntoImageView(state.cardArtworkUri, binding.imvFrontCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvFirstBackupCard)
loadImageIntoImageView(state.cardArtworkUri, binding.imvSecondBackupCard)
handleOnboardingStep(state)
}
}
}
@ -180,7 +205,12 @@ class OnboardingWalletFragment :
internal fun handleOnboardingStep(state: OnboardingWalletState) {
when (state.step) {
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
OnboardingWalletStep.Backup -> setBackupState(state.backupState, state.isSaltPay)
OnboardingWalletStep.Backup -> setBackupState(
state = state.backupState,
isSaltPay = state.isSaltPay,
isWallet2 = state.wallet2State != null,
)
else -> {}
}
}
@ -201,9 +231,9 @@ class OnboardingWalletFragment :
animator.setupCreateWalletState()
}
private fun setBackupState(state: BackupState, isSaltPay: Boolean) {
private fun setBackupState(state: BackupState, isSaltPay: Boolean, isWallet2: Boolean) {
when (state.backupStep) {
BackupStep.InitBackup -> showBackupIntro(state, isSaltPay)
BackupStep.InitBackup -> showBackupIntro(state, isSaltPay, isWallet2)
BackupStep.ScanOriginCard -> showScanOriginCard()
BackupStep.AddBackupCards -> showAddBackupCards(state, isSaltPay)
BackupStep.SetAccessCode -> showSetAccessCode()
@ -215,7 +245,7 @@ class OnboardingWalletFragment :
}
}
private fun showBackupIntro(state: BackupState, isSaltPay: Boolean) = with(binding) {
private fun showBackupIntro(state: BackupState, isSaltPay: Boolean, isWallet2: Boolean) = with(binding) {
imvFirstBackupCard.show()
imvSecondBackupCard.show()
@ -230,7 +260,7 @@ class OnboardingWalletFragment :
btnWalletAlternativeAction.text = getText(R.string.onboarding_button_skip_backup)
btnWalletAlternativeAction.setOnClickListener { store.dispatch(BackupAction.SkipBackup) }
btnWalletAlternativeAction.show(state.canSkipBackup && !isSaltPay)
btnWalletAlternativeAction.show(state.canSkipBackup && !isSaltPay && !isWallet2)
}
animator.showBackupIntro(state)
}
@ -271,7 +301,7 @@ class OnboardingWalletFragment :
layoutButtonsAddCards.btnContinue.isEnabled = state.backupCardsNumber != 0
if (isSaltPay) {
onboardingSaltPayView.showAddBackupCards(state)
saltPayStateHandler.showAddBackupCards(state)
} else {
when (state.backupCardsNumber) {
0 -> {
@ -443,4 +473,47 @@ class OnboardingWalletFragment :
resources.getValue(R.dimen.device_scale_factor_for_twins_welcome, typedValue, true)
return typedValue.float
}
private fun makeSeedPhraseRouter(): SeedPhraseRouter = SeedPhraseRouter(
onBack = ::handleOnBackPressed,
onOpenChat = {
store.dispatch(GlobalAction.OpenChat(SupportInfo()))
},
onOpenUriClick = { uri ->
store.dispatchOpenUrl(uri.toString())
},
)
private fun makeSeedPhraseMediator(): SeedPhraseMediator {
return object : SeedPhraseMediator {
override fun createWallet(callback: (CompletionResult<CreateWalletResponse>) -> Unit) {
store.dispatch(OnboardingWallet2Action.CreateWallet(callback))
}
override fun onWalletCreated(result: CompletionResult<CreateWalletResponse>) {
store.dispatch(OnboardingWallet2Action.WalletWasCreated(result))
}
override fun importWallet(
mnemonicComponents: List<String>,
seedPhraseSource: SeedPhraseSource,
callback: (CompletionResult<CreateWalletResponse>) -> Unit,
) {
store.dispatch(OnboardingWallet2Action.ImportWallet(mnemonicComponents, seedPhraseSource, callback))
}
override fun allowScreenshots(allow: Boolean) {
mainScope.launch {
if (allow) {
requireActivity().window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
requireActivity().window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE,
)
}
}
}
}
}
}

View file

@ -0,0 +1,25 @@
package com.tangem.tap.features.onboarding.products.wallet.ui.dialogs
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.store
import com.tangem.wallet.R
object ResetBackupCardDialog {
fun create(context: Context, cardId: String): AlertDialog {
return AlertDialog.Builder(context).apply {
setTitle(R.string.common_warning)
setMessage(R.string.onboarding_linking_error_card_with_wallets)
setPositiveButton(R.string.common_continue) { _, _ ->
store.dispatch(BackupAction.ResetBackupCard(cardId))
}
setNegativeButton(R.string.common_cancel) { _, _ ->
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}
}

View file

@ -120,9 +120,16 @@ internal class SaveWalletMiddleware {
)
}
val savedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
Timber.e("User wallet is not saved")
return@launch
}
store.dispatchWithMain(SaveWalletAction.Save.Success)
store.dispatchWithMain(NavigationAction.PopBackTo(AppScreen.Wallet))
store.dispatchWithMain(WalletAction.UpdateCanSaveUserWallets(canSaveUserWallets = true))
store.dispatchWithMain(
action = WalletAction.MultiWallet.CheckForBackupWarning(savedUserWallet.scanResponse.card),
)
}
}
}

View file

@ -69,6 +69,10 @@ sealed class TransactionExtrasAction : SendScreenActionUi {
sealed class TonMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : TonMemo()
}
sealed class CosmosMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : CosmosMemo()
}
}
sealed class AddressPayIdVerifyAction : SendScreenAction {

View file

@ -82,7 +82,7 @@ class AmountMiddleware {
val sendState = appState?.sendState ?: return
dispatch(AmountAction.SetAmount(sendState.amountState.balanceCrypto, false))
if (sendState.amountState.isCoinAmount()) {
if (sendState.amountState.canIncludeFee()) {
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true))
dispatch(FeeActionUi.ChangeIncludeFee(true))
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.features.send.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.cosmos.CosmosTransactionExtras
import com.tangem.blockchain.blockchains.polkadot.ExistentialDepositProvider
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.ton.TonTransactionExtras
@ -171,6 +172,7 @@ private fun sendTransaction(
transactionExtras.xrpDestinationTag?.tag?.let {
txData = txData.copy(extras = XrpTransactionBuilder.XrpTransactionExtras(it))
}
transactionExtras.cosmosMemoState?.memo?.let { txData = txData.copy(extras = CosmosTransactionExtras(it)) }
transactionExtras.tonMemoState?.memo?.let { txData = txData.copy(extras = TonTransactionExtras(it)) }
scope.launch {

View file

@ -1,20 +1,10 @@
package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.FeeActionUi
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.ReleaseSendState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.IdStateHolder
import com.tangem.tap.features.send.redux.states.SendState
@ -98,21 +88,28 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer {
val walletManager = action.walletManager!!
val amountToExtract = prepareAction.tokenAmount ?: prepareAction.coinAmount!!
val decimals = amountToExtract.decimals
val feePaidInNetworkCurrency = isFeePaidInNetworkCurrency(walletManager.wallet.blockchain)
return sendState.copy(
walletManager = walletManager,
coinConverter = action.coinRate?.let { CurrencyConverter(it, decimals) },
tokenConverter = action.tokenRate?.let { CurrencyConverter(it, decimals) },
amountState = sendState.amountState.copy(
feePaidInCurrencyNetworkCurrency = feePaidInNetworkCurrency,
amountToExtract = amountToExtract,
typeOfAmount = amountToExtract.type,
balanceCrypto = amountToExtract.value ?: BigDecimal.ZERO,
),
feeState = sendState.feeState.copy(
includeFeeSwitcherIsEnabled = amountToExtract.type == AmountType.Coin,
includeFeeSwitcherIsEnabled = feePaidInNetworkCurrency || isCoinAmount(amountToExtract.type),
),
)
}
private fun isFeePaidInNetworkCurrency(blockchain: Blockchain): Boolean =
blockchain.tokenTransactionFeePaidInNetworkCurrency()
private fun isCoinAmount(typeOfAmount: AmountType): Boolean = typeOfAmount == AmountType.Coin
}
internal fun updateLastState(sendState: SendState, lastChangedState: IdStateHolder): SendState {

View file

@ -3,21 +3,8 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.blockchains.stellar.StellarMemo
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction.BinanceMemo
import com.tangem.tap.features.send.redux.TransactionExtrasAction.Prepare
import com.tangem.tap.features.send.redux.TransactionExtrasAction.Release
import com.tangem.tap.features.send.redux.TransactionExtrasAction.TonMemo
import com.tangem.tap.features.send.redux.TransactionExtrasAction.XlmMemo
import com.tangem.tap.features.send.redux.TransactionExtrasAction.XrpDestinationTag
import com.tangem.tap.features.send.redux.states.BinanceMemoState
import com.tangem.tap.features.send.redux.states.InputViewValue
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.send.redux.states.TonMemoState
import com.tangem.tap.features.send.redux.states.TransactionExtraError
import com.tangem.tap.features.send.redux.states.TransactionExtrasState
import com.tangem.tap.features.send.redux.states.XlmMemoState
import com.tangem.tap.features.send.redux.states.XlmMemoType
import com.tangem.tap.features.send.redux.states.XrpDestinationTagState
import com.tangem.tap.features.send.redux.TransactionExtrasAction.*
import com.tangem.tap.features.send.redux.states.*
/**
[REDACTED_AUTHOR]
@ -31,6 +18,7 @@ class TransactionExtrasReducer : SendInternalReducer {
is BinanceMemo -> handleBinanceMemo(action, sendState, sendState.transactionExtrasState)
is XrpDestinationTag -> handleXrpTag(action, sendState, sendState.transactionExtrasState)
is TonMemo -> handleTonMemo(action, sendState, sendState.transactionExtrasState)
is CosmosMemo -> handleCosmosMemo(action, sendState, sendState.transactionExtrasState)
else -> sendState
}
}
@ -60,6 +48,10 @@ class TransactionExtrasReducer : SendInternalReducer {
Blockchain.Stellar -> TransactionExtrasState(xlmMemo = XlmMemoState())
Blockchain.Binance -> TransactionExtrasState(binanceMemo = BinanceMemoState())
Blockchain.TON, Blockchain.TONTestnet -> TransactionExtrasState(tonMemoState = TonMemoState())
Blockchain.Cosmos,
Blockchain.TerraV1,
Blockchain.TerraV2,
-> TransactionExtrasState(cosmosMemoState = CosmosMemoState())
else -> emptyResult
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
@ -155,4 +147,19 @@ class TransactionExtrasReducer : SendInternalReducer {
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleCosmosMemo(
action: CosmosMemo,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is CosmosMemo.HandleUserInput -> {
val memo = action.data
val input = InputViewValue(memo, true)
infoState.copy(cosmosMemoState = CosmosMemoState(input, memo))
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
}

View file

@ -31,6 +31,7 @@ data class TransactionExtrasState(
val binanceMemo: BinanceMemoState? = null,
val xrpDestinationTag: XrpDestinationTagState? = null,
val tonMemoState: TonMemoState? = null,
val cosmosMemoState: CosmosMemoState? = null,
) : IdStateHolder {
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
}
@ -104,6 +105,11 @@ data class TonMemoState(
val memo: String? = null,
val error: TransactionExtraError? = null,
)
data class CosmosMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: String? = null,
val error: TransactionExtraError? = null,
)
enum class TransactionExtraError {
INVALID_DESTINATION_TAG,

View file

@ -91,7 +91,7 @@ data class SendState(
fun getButtonState(): ButtonState = if (isReadyToSend()) ButtonState.ENABLED else ButtonState.DISABLED
fun getTotalAmountToSend(value: BigDecimal = amountState.amountToSendCrypto): BigDecimal {
val needToExtractFee = amountState.isCoinAmount() && feeState.feeIsIncluded
val needToExtractFee = amountState.canIncludeFee() && feeState.feeIsIncluded
return if (needToExtractFee) value.minus(feeState.getCurrentFeeValue()) else value
}
@ -135,13 +135,16 @@ data class AmountState(
val decimalSeparator: String = ".",
val error: TapError? = null,
val inputIsEnabled: Boolean = true,
private val feePaidInCurrencyNetworkCurrency: Boolean = false,
) : SendScreenState {
override val stateId: StateId = StateId.AMOUNT
fun isReady(): Boolean = error == null && !amountToSendCrypto.isZero()
fun isCoinAmount(): Boolean = typeOfAmount == AmountType.Coin
private fun isCoinAmount(): Boolean = typeOfAmount == AmountType.Coin
fun canIncludeFee(): Boolean = isCoinAmount() || feePaidInCurrencyNetworkCurrency
fun createMainCurrency(type: MainCurrencyType, canSwitched: Boolean): MainCurrency {
return if (!canSwitched) {

View file

@ -60,6 +60,7 @@ import com.tangem.tap.mainScope
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentSendBinding
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
@ -69,9 +70,12 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import java.text.DecimalFormatSymbols
private const val EDIT_TEXT_INPUT_DEBOUNCE = 400L
/**
[REDACTED_AUTHOR]
*/
@OptIn(FlowPreview::class)
class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
lateinit var sendBtn: ViewStateWidget
@ -115,16 +119,15 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
sendBtn = IndeterminateProgressButtonWidget(btnSend, progress)
}
@Suppress("MagicNumber")
private fun setupAddressOrPayIdLayout() = with(binding.lSendAddressPayid) {
store.dispatch(SetTruncateHandler { etAddressOrPayId.truncateMiddleWith(it, "...") })
store.dispatch(CheckClipboard(requireContext().getFromClipboard()?.toString()))
etAddressOrPayId.setOnFocusChangeListener { v, hasFocus ->
etAddressOrPayId.setOnFocusChangeListener { _, hasFocus ->
store.dispatch(TruncateOrRestore(!hasFocus))
}
etAddressOrPayId.inputtedTextAsFlow()
.debounce(400)
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.addressPayIdState.viewFieldValue.value != it }
.onEach {
store.dispatch(AddressPayIdActionUi.HandleUserInput(it))
@ -146,8 +149,9 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
private fun setupTransactionExtrasLayout() = with(binding.lSendAddressPayid) {
// TODO: [REDACTED_TASK_KEY]
etXlmMemo.inputtedTextAsFlow()
.debounce(400)
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xlmMemo?.viewFieldValue?.value != it
@ -156,7 +160,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
.launchIn(mainScope)
etDestinationTag.inputtedTextAsFlow()
.debounce(400)
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.xrpDestinationTag?.viewFieldValue?.value != it
@ -165,7 +169,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
.launchIn(mainScope)
etBinanceMemo.inputtedTextAsFlow()
.debounce(400)
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.binanceMemo?.viewFieldValue?.value != it
@ -174,13 +178,22 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
.launchIn(mainScope)
etTonMemo.inputtedTextAsFlow()
.debounce(400)
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.tonMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.TonMemo.HandleUserInput(it)) }
.launchIn(mainScope)
etCosmosMemo.inputtedTextAsFlow()
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter {
val info = store.state.sendState.transactionExtrasState
info.cosmosMemoState?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.CosmosMemo.HandleUserInput(it)) }
.launchIn(mainScope)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@ -234,7 +247,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
etAmountToSend.keyListener = DigitsKeyListener.getInstance("0123456789,.")
etAmountToSend.setOnFocusChangeListener { v, hasFocus ->
etAmountToSend.setOnFocusChangeListener { _, hasFocus ->
snackbarControlledByChangingFocus = true
if (hasFocus) {
etAmountToSend.postDelayed(200) {
@ -257,7 +270,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
etAmountToSend.inputtedTextAsFlow()
.debounce(400)
.debounce(EDIT_TEXT_INPUT_DEBOUNCE)
.filter { store.state.sendState.amountState.viewAmountValue.value != it && it.isNotEmpty() }
.onEach { store.dispatch(AmountActionUi.HandleUserInput(it)) }
.launchIn(mainScope)
@ -273,13 +286,13 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
store.dispatch(ToggleControlsVisibility)
}
chipGroup.check(FeeUiHelper.toId(FeeType.NORMAL))
chipGroup.setOnCheckedChangeListener { group, checkedId ->
chipGroup.setOnCheckedChangeListener { _, checkedId ->
if (checkedId == -1) return@setOnCheckedChangeListener
store.dispatch(ChangeSelectedFee(FeeUiHelper.toType(checkedId)))
store.dispatch(CheckAmountToSend)
}
swIncludeFee.setOnCheckedChangeListener { btn, isChecked ->
swIncludeFee.setOnCheckedChangeListener { _, isChecked ->
store.dispatch(ChangeIncludeFee(isChecked))
store.dispatch(CheckAmountToSend)
}

View file

@ -80,6 +80,7 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
showView(xrpDestinationTagContainer, infoState.xrpDestinationTag)
showView(binanceMemoContainer, infoState.binanceMemo)
showView(tonMemoContainer, infoState.tonMemoState)
showView(cosmosMemoContainer, infoState.cosmosMemoState)
infoState.xlmMemo?.let {
if (!it.viewFieldValue.isFromUserInput) etXlmMemo.setText(it.viewFieldValue.value)
@ -126,6 +127,16 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
etTonMemo.setText(it.viewFieldValue.value)
}
}
infoState.cosmosMemoState?.let {
if (infoState.cosmosMemoState.error != null) {
tilCosmosMemo.error = fg.getText(R.string.send_extras_error_invalid_memo)
} else {
tilBinanceMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
etCosmosMemo.setText(it.viewFieldValue.value)
}
}
}
private fun handleSendScreen(fg: SendFragment, state: SendState) = with(fg.binding) {

View file

@ -1,12 +0,0 @@
package com.tangem.tap.features.tokens.api.featuretoggles
/**
* TokensList feature toggles
*
[REDACTED_AUTHOR]
*/
interface TokensListFeatureToggles {
/** Availability of redesigned screen (internal feature) */
val isRedesignedScreenEnabled: Boolean
}

View file

@ -1,24 +0,0 @@
package com.tangem.tap.features.tokens.impl.di
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
import com.tangem.tap.features.tokens.impl.featuretoggles.DefaultTokensListFeatureToggles
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 TokensListFeatureTogglesModule {
@Provides
@Singleton
fun providesTokensListFeatureToggles(featureTogglesManager: FeatureTogglesManager): TokensListFeatureToggles {
return DefaultTokensListFeatureToggles(featureTogglesManager)
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.tokens.impl.di
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
import dagger.Module
@ -17,5 +18,7 @@ internal object TokensListRouterModule {
@Provides
@ViewModelScoped
fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter()
fun provideTokensListRouter(customTokenFeatureToggles: CustomTokenFeatureToggles): TokensListRouter {
return DefaultTokensListRouter(customTokenFeatureToggles)
}
}

View file

@ -1,19 +0,0 @@
package com.tangem.tap.features.tokens.impl.featuretoggles
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
import com.tangem.tap.features.tokens.api.featuretoggles.TokensListFeatureToggles
/**
* Default implementation of TokensList feature toggles
*
* @property featureTogglesManager manager for getting information about the availability of feature toggles
*
[REDACTED_AUTHOR]
*/
internal class DefaultTokensListFeatureToggles(
private val featureTogglesManager: FeatureTogglesManager,
) : TokensListFeatureToggles {
override val isRedesignedScreenEnabled: Boolean
get() = featureTogglesManager.isFeatureEnabled(name = "REDESIGNED_TOKEN_LIST_SCREEN_ENABLED")
}

View file

@ -5,6 +5,7 @@ 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.core.view.WindowCompat
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel
@ -35,7 +36,9 @@ internal class TokensListFragment : Fragment() {
setContent {
isTransitionGroup = true
val viewModel = hiltViewModel<TokensListViewModel>()
val viewModel = hiltViewModel<TokensListViewModel>().apply {
LocalLifecycleOwner.current.lifecycle.addObserver(this)
}
TangemTheme {
TokensListScreen(stateHolder = viewModel.uiState)

View file

@ -12,7 +12,7 @@ import com.tangem.tap.store
*/
class TokensListArgs {
/** Tokens list screen mode */
val isManageAccess: Boolean get() = store.state.tokensState.allowToAdd
val isManageAccess: Boolean get() = store.state.tokensState.isManageAccess
/** Tokens list that accessible from the main screen */
val mainScreenTokenList: List<TokenWithBlockchain> get() = store.state.tokensState.addedTokens

View file

@ -3,7 +3,9 @@ package com.tangem.tap.features.tokens.impl.presentation.router
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
import com.tangem.tap.features.tokens.legacy.redux.TokensAction
import com.tangem.tap.features.wallet.redux.models.WalletDialog
import com.tangem.tap.store
@ -15,15 +17,20 @@ import com.tangem.wallet.R
*
[REDACTED_AUTHOR]
*/
internal class DefaultTokensListRouter : TokensListRouter {
internal class DefaultTokensListRouter(
private val customTokenFeatureToggles: CustomTokenFeatureToggles,
) : TokensListRouter {
override fun popBackStack() {
store.dispatch(NavigationAction.PopBackTo())
store.dispatch(TokensAction.ResetState)
}
override fun openAddCustomTokenScreen() {
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
if (customTokenFeatureToggles.isRedesignedScreenEnabled) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddCustomToken))
} else {
store.dispatch(TokensAction.PrepareAndNavigateToAddCustomToken)
}
}
override fun showAddressCopiedNotification() {

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.MutableState
import com.tangem.blockchain.common.Blockchain
import com.tangem.core.ui.extensions.getActiveIconRes
import com.tangem.tap.common.extensions.getGreyedOutIconRes
import com.tangem.tap.features.tokens.legacy.redux.ContractAddress
/**
* Network item state
@ -62,7 +61,7 @@ sealed interface NetworkItemState {
override val isMainNetwork: Boolean,
val isAdded: MutableState<Boolean>,
val id: String,
val address: ContractAddress?,
val address: String?,
val decimalCount: Int?,
val blockchain: Blockchain,
val onToggleClick: (TokenItemState.ManageContent, ManageContent) -> Unit,

View file

@ -9,6 +9,9 @@ import kotlinx.collections.immutable.ImmutableList
*/
sealed interface TokenItemState {
/** Token id */
val id: String
/** Token full name (name with symbol) */
val fullName: String
@ -21,11 +24,13 @@ sealed interface TokenItemState {
/**
* Token item state that is available for read
*
* @property fullName token name
* @property id token id
* @property fullName token name
* @property iconUrl token icon url
* @property networks list of networks that is available for read
*/
data class ReadContent(
override val id: String,
override val fullName: String,
override val iconUrl: String,
override val networks: ImmutableList<NetworkItemState.ReadContent>,
@ -34,18 +39,18 @@ sealed interface TokenItemState {
/**
* Token item state that is available for read and manage
*
* @property id token id
* @property fullName token name
* @property iconUrl token icon url
* @property networks list of networks is available for read and edit
* @property id token id
* @property name token name
* @property symbol token brief name
*/
data class ManageContent(
override val id: String,
override val fullName: String,
override val iconUrl: String,
override val networks: ImmutableList<NetworkItemState.ManageContent>,
val id: String,
val name: String,
val symbol: String,
) : TokenItemState

View file

@ -37,9 +37,9 @@ internal fun BriefNetworksList(
exit = fadeOut(),
) {
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
networks.forEach {
key(NetworkItemState::name) {
BriefNetworkItem(model = it)
networks.forEach { network ->
key(network.name + network.protocolName) {
BriefNetworkItem(model = network)
}
}
}

View file

@ -1,21 +1,10 @@
package com.tangem.tap.features.tokens.impl.presentation.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.*
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.*
import androidx.compose.material.Switch
import androidx.compose.material.SwitchDefaults
import androidx.compose.material.Text
@ -56,7 +45,7 @@ internal fun DetailedNetworksList(
) {
Column {
networks.forEachIndexed { index, network ->
key(network.name) {
key(network.name + network.protocolName) {
DetailedNetworkItem(token = token, network = network, isLastItem = networks.size - 1 == index)
}
}

View file

@ -10,12 +10,9 @@ import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.with
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
@ -24,18 +21,17 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.constraintlayout.compose.Dimension
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.core.ui.components.CurrencyPlaceholderIcon
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.compose.extensions.toPx
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
@ -134,27 +130,11 @@ private fun Icon(name: String, iconUrl: String, modifier: Modifier = Modifier) {
.crossfade(enable = true)
.build(),
contentDescription = null,
loading = { PlaceholderIcon(name = name, modifier = iconModifier) },
error = { PlaceholderIcon(name = name, modifier = iconModifier) },
loading = { CurrencyPlaceholderIcon(id = name, modifier = iconModifier) },
error = { CurrencyPlaceholderIcon(id = name, modifier = iconModifier) },
)
}
@Composable
private fun PlaceholderIcon(name: String, modifier: Modifier = Modifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.background(TangemTheme.colors.icon.primary1, shape = CircleShape),
) {
Text(
text = name.firstOrNull()?.titlecase() ?: "",
color = TangemTheme.colors.icon.primary2,
maxLines = 1,
// FIXME("Incorrect typography. Replace with typography from design system")
style = TangemTheme.typography.h3.copy(fontWeight = FontWeight.Bold),
)
}
}
@Composable
private fun Title(title: String, modifier: Modifier = Modifier) {
Text(

Some files were not shown because too many files have changed in this diff Show more