Updated on 2026-08-14
This commit is contained in:
commit
369ed25ac6
515 changed files with 6100 additions and 12756 deletions
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.api.featuretoggles
|
||||
|
||||
/**
|
||||
* Add custom token feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface CustomTokenFeatureToggles {
|
||||
|
||||
val isNewCardScanningEnabled: Boolean
|
||||
}
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.data
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.tap.features.customtoken.impl.data.converters.FoundTokenConverter
|
||||
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenRepository
|
||||
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
/**
|
||||
* Default implementation of custom token repository
|
||||
*
|
||||
* @property tangemTechApi TangemTech API
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property reduxStateHolder redux state holder
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DefaultCustomTokenRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val reduxStateHolder: AppStateHolder,
|
||||
) : CustomTokenRepository {
|
||||
|
||||
override suspend fun findToken(address: String, networkId: String?): FoundToken {
|
||||
val scanResponse = requireNotNull(reduxStateHolder.scanResponse)
|
||||
val supportedTokenNetworkIds = requireNotNull(scanResponse.card)
|
||||
.supportedBlockchains(scanResponse.cardTypesResolver)
|
||||
.filter(Blockchain::canHandleTokens)
|
||||
.map(Blockchain::toNetworkId)
|
||||
|
||||
return withContext(dispatchers.io) {
|
||||
val foundCoin = tangemTechApi.getCoins(
|
||||
contractAddress = address,
|
||||
networkIds = networkId ?: supportedTokenNetworkIds.joinToString(separator = ","),
|
||||
)
|
||||
.getOrThrow()
|
||||
.coins.firstNotNullOfOrNull { coin ->
|
||||
val networksWithTheSameAddress = coin.networks.filter { network ->
|
||||
(network.contractAddress != null || network.decimalCount != null) &&
|
||||
network.contractAddress?.equals(address, ignoreCase = true) == true &&
|
||||
supportedTokenNetworkIds.contains(network.networkId)
|
||||
}
|
||||
|
||||
if (networksWithTheSameAddress.isNotEmpty()) {
|
||||
coin.copy(networks = networksWithTheSameAddress)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
foundCoin?.let(FoundTokenConverter::convert) ?: error("Token not found")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.data.converters
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter between data model [CoinsResponse.Coin] and domain model [FoundToken]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object FoundTokenConverter : Converter<CoinsResponse.Coin, FoundToken> {
|
||||
|
||||
override fun convert(value: CoinsResponse.Coin): FoundToken {
|
||||
return FoundToken(
|
||||
id = value.id,
|
||||
name = value.name,
|
||||
symbol = value.symbol,
|
||||
isActive = value.active,
|
||||
network = value.networks.firstOrNull()?.let { network ->
|
||||
FoundToken.Network(
|
||||
id = network.networkId,
|
||||
contractAddress = requireNotNull(network.contractAddress),
|
||||
decimalCount = requireNotNull(network.decimalCount).toString(),
|
||||
)
|
||||
} ?: error("Found token networks is empty"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.di
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
import com.tangem.tap.features.customtoken.impl.featuretoggles.DefaultCustomTokenFeatureToggles
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object CustomTokenFeatureTogglesModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesCustomTokenFeatureToggles(featureTogglesManager: FeatureTogglesManager): CustomTokenFeatureToggles {
|
||||
return DefaultCustomTokenFeatureToggles(featureTogglesManager)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.di
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.features.customtoken.impl.data.DefaultCustomTokenRepository
|
||||
import com.tangem.tap.features.customtoken.impl.domain.CustomTokenInteractor
|
||||
import com.tangem.tap.features.customtoken.impl.domain.DefaultCustomTokenInteractor
|
||||
import com.tangem.tap.proxy.AppStateHolder
|
||||
import com.tangem.utils.coroutines.AppCoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
internal object CustomTokenInteractorModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideCustomTokenInteractor(
|
||||
tangemTechApi: TangemTechApi,
|
||||
appCoroutineDispatcherProvider: AppCoroutineDispatcherProvider,
|
||||
reduxStateHolder: AppStateHolder,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
): CustomTokenInteractor {
|
||||
return DefaultCustomTokenInteractor(
|
||||
featureRepository = DefaultCustomTokenRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = appCoroutineDispatcherProvider,
|
||||
reduxStateHolder = reduxStateHolder,
|
||||
),
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
derivePublicKeysUseCase = derivePublicKeysUseCase,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.di
|
||||
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.routers.CustomTokenRouter
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.routers.DefaultCustomTokenRouter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
internal object CustomTokenRouterModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideAddCustomTokenRouter(): CustomTokenRouter = DefaultCustomTokenRouter()
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.domain
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
||||
|
||||
/**
|
||||
* Custom token interactor
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface CustomTokenInteractor {
|
||||
|
||||
/** Find token by [address] and [blockchain] */
|
||||
suspend fun findToken(address: String, blockchain: Blockchain): FoundToken
|
||||
|
||||
/** Save token [customCurrency] */
|
||||
@Throws(TangemError::class)
|
||||
suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit>
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.domain
|
||||
|
||||
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
||||
|
||||
/**
|
||||
* Custom token repository
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
interface CustomTokenRepository {
|
||||
|
||||
/**
|
||||
* Find token by [address] and [networkId]
|
||||
*
|
||||
* @throws com.tangem.datasource.api.common.response.ApiResponseError
|
||||
* */
|
||||
suspend fun findToken(address: String, networkId: String?): FoundToken
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.domain
|
||||
|
||||
import arrow.core.getOrElse
|
||||
import arrow.core.raise.result
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.card.DerivePublicKeysUseCase
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.features.addCustomToken.CustomCurrency
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.domain.model.Currency
|
||||
import com.tangem.tap.features.customtoken.impl.domain.models.FoundToken
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
|
||||
/**
|
||||
* Default implementation of custom token interactor
|
||||
*
|
||||
* @property featureRepository feature repository
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class DefaultCustomTokenInteractor(
|
||||
private val featureRepository: CustomTokenRepository,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val derivePublicKeysUseCase: DerivePublicKeysUseCase,
|
||||
) : CustomTokenInteractor {
|
||||
|
||||
// TODO: Move to DI
|
||||
private val addCryptoCurrenciesUseCase by lazy(LazyThreadSafetyMode.NONE) {
|
||||
val currenciesRepository = store.inject(DaggerGraphState::currenciesRepository)
|
||||
val networksRepository = store.inject(DaggerGraphState::networksRepository)
|
||||
|
||||
AddCryptoCurrenciesUseCase(currenciesRepository, networksRepository)
|
||||
}
|
||||
|
||||
override suspend fun findToken(address: String, blockchain: Blockchain): FoundToken {
|
||||
return featureRepository.findToken(
|
||||
address = address,
|
||||
networkId = if (blockchain != Blockchain.Unknown) blockchain.toNetworkId() else null,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun saveToken(customCurrency: CustomCurrency): Result<Unit> {
|
||||
return result {
|
||||
val userWallet = getSelectedWalletSyncUseCase().getOrElse {
|
||||
error("Failed to get selected wallet: $it")
|
||||
}
|
||||
|
||||
val currency = Currency.fromCustomCurrency(customCurrency)
|
||||
val currencies = listOfNotNull(element = currency.toCryptoCurrency(userWallet.scanResponse))
|
||||
|
||||
derivePublicKeysUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind()
|
||||
addCryptoCurrenciesUseCase(userWalletId = userWallet.walletId, currencies = currencies).bind()
|
||||
}
|
||||
}
|
||||
|
||||
private fun Currency.toCryptoCurrency(scanResponse: ScanResponse): CryptoCurrency? {
|
||||
val cryptoCurrencyFactory = CryptoCurrencyFactory()
|
||||
|
||||
return when (this) {
|
||||
is Currency.Blockchain -> {
|
||||
cryptoCurrencyFactory.createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = derivationPath,
|
||||
derivationStyleProvider = scanResponse.derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
is Currency.Token -> {
|
||||
cryptoCurrencyFactory.createToken(
|
||||
sdkToken = token,
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = derivationPath,
|
||||
derivationStyleProvider = scanResponse.derivationStyleProvider,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.domain.models
|
||||
|
||||
/**
|
||||
* Found token model
|
||||
*
|
||||
* @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 isActive: Boolean,
|
||||
val network: Network,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Found token network
|
||||
*
|
||||
* @property id id
|
||||
* @property contractAddress address
|
||||
* @property decimalCount decimal count
|
||||
*/
|
||||
data class Network(val id: String, val contractAddress: String, val decimalCount: String)
|
||||
}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.tap.features.customtoken.api.featuretoggles.CustomTokenFeatureToggles
|
||||
|
||||
/**
|
||||
* Default implementation of CustomToken feature toggles
|
||||
*
|
||||
* @property featureTogglesManager manager for getting information about the availability of feature toggles
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultCustomTokenFeatureToggles(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) : CustomTokenFeatureToggles {
|
||||
|
||||
override val isNewCardScanningEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "NEW_CARD_SCANNING_ENABLED")
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.ui.AddCustomTokenScreen
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.viewmodels.AddCustomTokenViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Add custom token screen
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
internal class AddCustomTokenFragment : ComposeFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var uiDependencies: UiDependencies
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val viewModel = hiltViewModel<AddCustomTokenViewModel>().apply {
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(this)
|
||||
}
|
||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||
|
||||
AddCustomTokenScreen(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.systemBarsPadding(),
|
||||
stateHolder = state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,324 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.models
|
||||
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
* Toolbar model of add custom token screen
|
||||
*
|
||||
* @property title title
|
||||
* @property onBackButtonClick lambda be invoked when back button is been pressed
|
||||
*/
|
||||
internal data class AddCustomTokensToolbar(val title: TextReference, val onBackButtonClick: () -> Unit)
|
||||
|
||||
/**
|
||||
* Model of block with fields for testing
|
||||
*
|
||||
* @property chooseTokenButtonText choose token button text
|
||||
* @property clearButtonText clear button text
|
||||
* @property resetButtonText reset button text
|
||||
* @property onClearAddressButtonClick lambda be invoked when clear address button is been pressed
|
||||
* @property onResetButtonClick lambda be invoked when reset form fields button is been pressed
|
||||
*/
|
||||
internal data class AddCustomTokenTestBlock(
|
||||
val chooseTokenButtonText: String,
|
||||
val clearButtonText: String,
|
||||
val resetButtonText: String,
|
||||
val onClearAddressButtonClick: () -> Unit,
|
||||
val onResetButtonClick: () -> Unit,
|
||||
)
|
||||
|
||||
/**
|
||||
* Bottom sheet model for choose custom token
|
||||
*
|
||||
* @property categoriesBlocks tokens categories
|
||||
* @property onTestTokenClick lambda be invoked when token is been pressed
|
||||
*/
|
||||
internal data class AddCustomTokenChooseTokenBottomSheet(
|
||||
val categoriesBlocks: List<TokensCategoryBlock>,
|
||||
val onTestTokenClick: (String) -> Unit,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Tokens category model
|
||||
*
|
||||
* @property name category name
|
||||
* @property items category items
|
||||
*/
|
||||
data class TokensCategoryBlock(val name: String, val items: List<TestTokenItem>)
|
||||
|
||||
/**
|
||||
* Test token model
|
||||
*
|
||||
* @property name token name
|
||||
* @property address token address
|
||||
*/
|
||||
data class TestTokenItem(val name: String, val address: String)
|
||||
}
|
||||
|
||||
/**
|
||||
* Form with fields model of add custom token screen
|
||||
*
|
||||
* @property contractAddressInputField input field to enter the contract address
|
||||
* @property networkSelectorField selector field to select the token network
|
||||
* @property tokenNameInputField input field to enter the token name
|
||||
* @property tokenSymbolInputField input field to enter the token symbol
|
||||
* @property decimalsInputField input field to enter the token decimals
|
||||
* @property derivationPathSelectorField selector field to select the derivation path
|
||||
* @property derivationPathInputField input field for a custom derivation path
|
||||
* @property showTokenFields if token fields should be shown
|
||||
*/
|
||||
internal data class AddCustomTokenForm(
|
||||
val contractAddressInputField: AddCustomTokenInputField.ContactAddress,
|
||||
val networkSelectorField: AddCustomTokenSelectorField.Network,
|
||||
val tokenNameInputField: AddCustomTokenInputField.TokenName,
|
||||
val tokenSymbolInputField: AddCustomTokenInputField.TokenSymbol,
|
||||
val decimalsInputField: AddCustomTokenInputField.Decimals,
|
||||
val derivationPathSelectorField: AddCustomTokenSelectorField.DerivationPath?,
|
||||
val derivationPathInputField: AddCustomTokenInputField.DerivationPath?,
|
||||
val showTokenFields: Boolean = false,
|
||||
)
|
||||
|
||||
/** Base input field model of add custom token screen */
|
||||
internal sealed interface AddCustomTokenInputField {
|
||||
|
||||
/** Current value */
|
||||
val value: String
|
||||
|
||||
/** Lambda be invoked when value is been changed */
|
||||
val onValueChange: (String) -> Unit
|
||||
|
||||
/** Keyboard options */
|
||||
val keyboardOptions: KeyboardOptions
|
||||
|
||||
/** Label */
|
||||
val label: TextReference
|
||||
|
||||
/** Placeholder (hint) */
|
||||
val placeholder: TextReference
|
||||
|
||||
/**
|
||||
* Input field model to enter the contract address
|
||||
*
|
||||
* @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 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 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 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 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 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 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 keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val isEnabled: Boolean,
|
||||
) : AddCustomTokenInputField
|
||||
|
||||
/**
|
||||
* Input field model to enter a custom derivation path
|
||||
*
|
||||
* @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 showField whether the field should be shown
|
||||
*/
|
||||
data class DerivationPath(
|
||||
override val value: String,
|
||||
override val onValueChange: (String) -> Unit,
|
||||
override val keyboardOptions: KeyboardOptions,
|
||||
override val label: TextReference,
|
||||
override val placeholder: TextReference,
|
||||
val showField: Boolean = false,
|
||||
) : AddCustomTokenInputField
|
||||
}
|
||||
|
||||
/** Base selector field model of add custom token screen */
|
||||
internal sealed interface AddCustomTokenSelectorField {
|
||||
|
||||
/** Label */
|
||||
val label: TextReference
|
||||
|
||||
/** Selected menu item */
|
||||
val selectedItem: SelectorItem
|
||||
|
||||
/** Menu items */
|
||||
val items: List<SelectorItem>
|
||||
|
||||
/** Lambda be invoked when menu item is been selected */
|
||||
val onMenuItemClick: (Int) -> Unit
|
||||
|
||||
/**
|
||||
* Network selector model
|
||||
*
|
||||
* @property 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
|
||||
|
||||
/**
|
||||
* Derivation path 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
|
||||
* @property isEnabled selection availability
|
||||
*/
|
||||
data class DerivationPath(
|
||||
override val label: TextReference,
|
||||
override val selectedItem: SelectorItem.TitleWithSubtitle,
|
||||
override val items: List<SelectorItem.TitleWithSubtitle>,
|
||||
override val onMenuItemClick: (Int) -> Unit,
|
||||
val isEnabled: Boolean,
|
||||
) : AddCustomTokenSelectorField
|
||||
|
||||
/** Base menu item model */
|
||||
sealed interface SelectorItem {
|
||||
|
||||
/** Title */
|
||||
val title: TextReference
|
||||
|
||||
/** Blockchain */
|
||||
val blockchain: Blockchain
|
||||
|
||||
/**
|
||||
* Menu item with title
|
||||
*
|
||||
* @property title title text
|
||||
* @property blockchain blockchain
|
||||
*/
|
||||
data class Title(override val title: TextReference, override val blockchain: Blockchain) : SelectorItem
|
||||
|
||||
/**
|
||||
* Menu item with title ans subtitle
|
||||
*
|
||||
* @property title title text
|
||||
* @property blockchain blockchain
|
||||
* @property subtitle subtitle text
|
||||
*/
|
||||
data class TitleWithSubtitle(
|
||||
override val title: TextReference,
|
||||
override val blockchain: Blockchain,
|
||||
val subtitle: TextReference,
|
||||
val type: DerivationPathSelectorType = DerivationPathSelectorType.BLOCKCHAIN,
|
||||
) : SelectorItem
|
||||
}
|
||||
}
|
||||
|
||||
enum class DerivationPathSelectorType {
|
||||
DEFAULT, CUSTOM, BLOCKCHAIN
|
||||
}
|
||||
|
||||
/**
|
||||
* Warning model of add custom token screen
|
||||
*
|
||||
* @property description warning description
|
||||
*/
|
||||
internal sealed class AddCustomTokenWarning(val description: TextReference) {
|
||||
|
||||
/** Potential scam warning */
|
||||
data object PotentialScamToken : AddCustomTokenWarning(
|
||||
description = TextReference.Res(R.string.custom_token_validation_error_not_found),
|
||||
)
|
||||
|
||||
/** Token already added warning */
|
||||
data object TokenAlreadyAdded : AddCustomTokenWarning(
|
||||
description = TextReference.Res(R.string.custom_token_validation_error_already_added),
|
||||
)
|
||||
|
||||
/** Unsupported token warning */
|
||||
data class UnsupportedToken(val networkName: String) : AddCustomTokenWarning(
|
||||
description = TextReference.Res(R.string.alert_manage_tokens_unsupported_message, networkName),
|
||||
)
|
||||
|
||||
data object WrongDerivationPath : AddCustomTokenWarning(
|
||||
description = TextReference.Res(R.string.custom_token_invalid_derivation_path),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating button of add custom token screen
|
||||
*
|
||||
* @property isEnabled button availability
|
||||
* @property showProgress whether circle progress indication is enabled
|
||||
* @property onClick lambda be invoked when button is been pressed
|
||||
*/
|
||||
internal data class AddCustomTokenFloatingButton(
|
||||
val isEnabled: Boolean,
|
||||
val showProgress: Boolean,
|
||||
val onClick: () -> Unit,
|
||||
)
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.models
|
||||
|
||||
/** Custom token type */
|
||||
enum class CustomTokenType { TOKEN, BLOCKCHAIN }
|
||||
|
|
@ -1,6 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.models
|
||||
|
||||
internal enum class SupportBlockchainType {
|
||||
|
||||
SUPPORTED, UNSUPPORTED, UNABLE_TO_DETERMINE
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.routers
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
||||
/**
|
||||
* Custom token feature router
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal interface CustomTokenRouter {
|
||||
|
||||
/** Return to last screen */
|
||||
fun popBackStack()
|
||||
|
||||
/** Open wallet (main) screen */
|
||||
fun openWalletScreen()
|
||||
|
||||
/** Open alert if solana network is unsupported
|
||||
*
|
||||
* @param blockchain blockchain to show alert
|
||||
*/
|
||||
fun openUnsupportedNetworkAlert(blockchain: Blockchain)
|
||||
|
||||
fun showGenericErrorAlertAndPopBack()
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.routers
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.utils.popTo
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/** Default implementation of custom token feature router */
|
||||
internal class DefaultCustomTokenRouter : CustomTokenRouter {
|
||||
|
||||
override fun popBackStack() {
|
||||
store.dispatchNavigationAction(AppRouter::pop)
|
||||
}
|
||||
|
||||
override fun openWalletScreen() {
|
||||
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
|
||||
}
|
||||
|
||||
override fun openUnsupportedNetworkAlert(blockchain: Blockchain) {
|
||||
val alert = AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_warning,
|
||||
messageId = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
args = listOf(blockchain.getNetworkName()),
|
||||
)
|
||||
store.dispatchDialogShow(alert)
|
||||
}
|
||||
|
||||
override fun showGenericErrorAlertAndPopBack() {
|
||||
val alert = AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_error,
|
||||
messageId = R.string.common_unknown_error,
|
||||
onOk = { store.dispatchNavigationAction(AppRouter::pop) },
|
||||
)
|
||||
store.dispatchDialogShow(alert)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.states
|
||||
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenChooseTokenBottomSheet
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenForm
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenTestBlock
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenWarning
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokensToolbar
|
||||
|
||||
/**
|
||||
* State holder of add custom token screen
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal sealed interface AddCustomTokenStateHolder {
|
||||
|
||||
/** Lambda be invoked when system back action is been called */
|
||||
val onBackButtonClick: () -> Unit
|
||||
|
||||
/** Toolbar model */
|
||||
val toolbar: AddCustomTokensToolbar
|
||||
|
||||
/** Form model */
|
||||
val form: AddCustomTokenForm
|
||||
|
||||
/** Warnings */
|
||||
val warnings: Set<AddCustomTokenWarning>
|
||||
|
||||
/** Floating button model */
|
||||
val floatingButton: AddCustomTokenFloatingButton
|
||||
|
||||
/**
|
||||
* Util function that allow to make a copy
|
||||
*
|
||||
* @param onBackButtonClick lambda be invoked when system back action is been called
|
||||
* @param toolbar toolbar model
|
||||
* @param form form model
|
||||
* @param warnings warnings
|
||||
* @param floatingButton floating button model
|
||||
*/
|
||||
fun copySealed(
|
||||
onBackButtonClick: () -> Unit = this.onBackButtonClick,
|
||||
toolbar: AddCustomTokensToolbar = this.toolbar,
|
||||
form: AddCustomTokenForm = this.form,
|
||||
warnings: Set<AddCustomTokenWarning> = this.warnings,
|
||||
floatingButton: AddCustomTokenFloatingButton = this.floatingButton,
|
||||
): AddCustomTokenStateHolder {
|
||||
return when (this) {
|
||||
is Content -> copy(onBackButtonClick, toolbar, form, warnings, floatingButton)
|
||||
is TestContent -> copy(onBackButtonClick, toolbar, form, warnings, floatingButton)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Content state
|
||||
*
|
||||
* @property onBackButtonClick lambda be invoked when system back action is been called
|
||||
* @property toolbar toolbar model
|
||||
* @property form form model
|
||||
* @property warnings warnings
|
||||
* @property floatingButton floating button model
|
||||
*/
|
||||
data class Content(
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
override val toolbar: AddCustomTokensToolbar,
|
||||
override val form: AddCustomTokenForm,
|
||||
override val warnings: Set<AddCustomTokenWarning>,
|
||||
override val floatingButton: AddCustomTokenFloatingButton,
|
||||
) : AddCustomTokenStateHolder
|
||||
|
||||
/**
|
||||
* Content state with fields for testing
|
||||
*
|
||||
* @property onBackButtonClick lambda be invoked when system back action is been called
|
||||
* @property toolbar toolbar model
|
||||
* @property form form model
|
||||
* @property warnings warnings
|
||||
* @property floatingButton floating button model
|
||||
* @property testBlock test block model
|
||||
* @property bottomSheet bottom sheet model
|
||||
*/
|
||||
data class TestContent(
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
override val toolbar: AddCustomTokensToolbar,
|
||||
override val form: AddCustomTokenForm,
|
||||
override val warnings: Set<AddCustomTokenWarning>,
|
||||
override val floatingButton: AddCustomTokenFloatingButton,
|
||||
val testBlock: AddCustomTokenTestBlock,
|
||||
val bottomSheet: AddCustomTokenChooseTokenBottomSheet,
|
||||
) : AddCustomTokenStateHolder
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.FabPosition
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.runtime.*
|
||||
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 androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
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.AddCustomTokenWarnings
|
||||
|
||||
/**
|
||||
* Add custom token content
|
||||
*
|
||||
* @param state screen state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun AddCustomTokenContent(state: AddCustomTokenStateHolder.Content, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = state.onBackButtonClick)
|
||||
|
||||
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
AddCustomTokenToolbar(
|
||||
title = state.toolbar.title,
|
||||
onBackButtonClick = state.toolbar.onBackButtonClick,
|
||||
)
|
||||
},
|
||||
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)
|
||||
.padding(bottom = floatingButtonHeight)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
AddCustomTokenForm(model = state.form)
|
||||
|
||||
AddCustomTokenWarnings(warnings = state.warnings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenContent() {
|
||||
TangemThemePreview {
|
||||
AddCustomTokenContent(state = AddCustomTokenPreviewData.createContent())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,126 +0,0 @@
|
|||
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.*
|
||||
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.UnsupportedToken(networkName = "Solana"),
|
||||
)
|
||||
}
|
||||
|
||||
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_old),
|
||||
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,
|
||||
),
|
||||
derivationPathInputField = null,
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
showProgress = 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,
|
||||
showProgress = false,
|
||||
onClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
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.TangemThemePreview
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.states.AddCustomTokenStateHolder
|
||||
|
||||
/**
|
||||
* Add custom token screen
|
||||
*
|
||||
* @param stateHolder state holder
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun AddCustomTokenScreen(stateHolder: AddCustomTokenStateHolder, modifier: Modifier = Modifier) {
|
||||
when (stateHolder) {
|
||||
is AddCustomTokenStateHolder.Content -> AddCustomTokenContent(stateHolder, modifier)
|
||||
is AddCustomTokenStateHolder.TestContent -> AddCustomTokenTestContent(stateHolder, modifier)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenScreen(
|
||||
@PreviewParameter(AddCustomTokenScreenProvider::class) stateHolder: AddCustomTokenStateHolder,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
AddCustomTokenScreen(stateHolder)
|
||||
}
|
||||
}
|
||||
|
||||
private class AddCustomTokenScreenProvider : CollectionPreviewParameterProvider<AddCustomTokenStateHolder>(
|
||||
collection = listOf(
|
||||
AddCustomTokenPreviewData.createContent(),
|
||||
AddCustomTokenPreviewData.createTestContent(),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
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.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.core.ui.res.TangemThemePreview
|
||||
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.AddCustomTokenTestBlock
|
||||
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.AddCustomTokenWarnings
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* Add custom token content for testing
|
||||
*
|
||||
* @param state screen state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
internal fun AddCustomTokenTestContent(state: AddCustomTokenStateHolder.TestContent, modifier: Modifier = Modifier) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val bottomSheetScaffoldState = rememberBottomSheetScaffoldState(
|
||||
bottomSheetState = BottomSheetState(initialValue = BottomSheetValue.Collapsed, LocalDensity.current),
|
||||
)
|
||||
|
||||
BackHandler(
|
||||
onBack = {
|
||||
onBackButtonClicked(
|
||||
coroutineScope = coroutineScope,
|
||||
bottomSheetScaffoldState = bottomSheetScaffoldState,
|
||||
defaultAction = state.onBackButtonClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
var floatingButtonHeight by remember { mutableStateOf(0.dp) }
|
||||
BottomSheetScaffold(
|
||||
modifier = modifier,
|
||||
sheetContent = {
|
||||
SheetContent(
|
||||
coroutineScope = coroutineScope,
|
||||
bottomSheetScaffoldState = bottomSheetScaffoldState,
|
||||
model = state.bottomSheet,
|
||||
)
|
||||
},
|
||||
scaffoldState = bottomSheetScaffoldState,
|
||||
topBar = {
|
||||
AddCustomTokenToolbar(
|
||||
title = state.toolbar.title,
|
||||
onBackButtonClick = {
|
||||
onBackButtonClicked(
|
||||
coroutineScope,
|
||||
bottomSheetScaffoldState,
|
||||
defaultAction = state.toolbar.onBackButtonClick,
|
||||
)
|
||||
},
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
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,
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(paddingValues = it)
|
||||
.padding(bottom = floatingButtonHeight)
|
||||
.fillMaxSize(),
|
||||
) {
|
||||
TestBlock(
|
||||
state.testBlock,
|
||||
coroutineScope,
|
||||
bottomSheetScaffoldState,
|
||||
)
|
||||
|
||||
AddCustomTokenForm(model = state.form)
|
||||
|
||||
AddCustomTokenWarnings(warnings = state.warnings)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
private fun onBackButtonClicked(
|
||||
coroutineScope: CoroutineScope,
|
||||
bottomSheetScaffoldState: BottomSheetScaffoldState,
|
||||
defaultAction: () -> Unit,
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
if (bottomSheetScaffoldState.bottomSheetState.isExpanded) {
|
||||
bottomSheetScaffoldState.bottomSheetState.collapse()
|
||||
} else {
|
||||
defaultAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun SheetContent(
|
||||
model: AddCustomTokenChooseTokenBottomSheet,
|
||||
coroutineScope: CoroutineScope,
|
||||
bottomSheetScaffoldState: BottomSheetScaffoldState,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(LocalConfiguration.current.screenHeightDp.dp - TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
Hand()
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
model.categoriesBlocks.forEachIndexed { index, categoryBlock ->
|
||||
key(categoryBlock) {
|
||||
Column {
|
||||
TokensList(
|
||||
title = categoryBlock.name,
|
||||
tokens = categoryBlock.items,
|
||||
onTestTokenClick = { address ->
|
||||
model.onTestTokenClick(address)
|
||||
coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.collapse() }
|
||||
},
|
||||
)
|
||||
|
||||
if (model.categoriesBlocks.lastIndex != index) {
|
||||
Divider()
|
||||
SpacerH8()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokensList(title: String, tokens: List<TestTokenItem>, onTestTokenClick: (String) -> Unit) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing24,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
|
||||
tokens.forEach { token ->
|
||||
key(token) {
|
||||
PrimaryButton(
|
||||
text = token.name,
|
||||
onClick = { onTestTokenClick(token.address) },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.padding(bottom = TangemTheme.dimens.spacing8)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun TestBlock(
|
||||
model: AddCustomTokenTestBlock,
|
||||
coroutineScope: CoroutineScope,
|
||||
bottomSheetScaffoldState: BottomSheetScaffoldState,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.padding(top = TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
val softwareKeyboardController = LocalSoftwareKeyboardController.current
|
||||
PrimaryButton(
|
||||
text = model.chooseTokenButtonText,
|
||||
onClick = {
|
||||
softwareKeyboardController?.hide()
|
||||
coroutineScope.launch { bottomSheetScaffoldState.bottomSheetState.expand() }
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
PrimaryButton(
|
||||
text = model.clearButtonText,
|
||||
onClick = model.onClearAddressButtonClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
PrimaryButton(
|
||||
text = model.resetButtonText,
|
||||
onClick = model.onResetButtonClick,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenTestContent() {
|
||||
TangemThemePreview {
|
||||
AddCustomTokenTestContent(state = AddCustomTokenPreviewData.createTestContent())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.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.PrimaryButtonIconStart
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.customtoken.impl.presentation.models.AddCustomTokenFloatingButton
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
* Add custom token floating button. Attached above the keyboard.
|
||||
*
|
||||
* @param model button model
|
||||
* @param modifier modifier
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun AddCustomTokenFloatingButton(model: AddCustomTokenFloatingButton, modifier: Modifier = Modifier) {
|
||||
PrimaryButtonIconStart(
|
||||
modifier = modifier
|
||||
.imePadding()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.custom_token_add_token),
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
enabled = model.isEnabled,
|
||||
showProgress = model.showProgress,
|
||||
onClick = model.onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenFloatingButton(
|
||||
@PreviewParameter(AddCustomTokenFloatingButtonProvider::class) model: AddCustomTokenFloatingButton,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
AddCustomTokenFloatingButton(model)
|
||||
}
|
||||
}
|
||||
|
||||
private class AddCustomTokenFloatingButtonProvider : CollectionPreviewParameterProvider<AddCustomTokenFloatingButton>(
|
||||
listOf(
|
||||
AddCustomTokenFloatingButton(isEnabled = true, showProgress = false, onClick = {}),
|
||||
AddCustomTokenFloatingButton(isEnabled = false, showProgress = false, onClick = {}),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,212 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
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.core.ui.res.TangemThemePreview
|
||||
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.customtoken.impl.presentation.ui.AddCustomTokenPreviewData
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
|
||||
/**
|
||||
* Add custom token form
|
||||
*
|
||||
* @param model component model
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun AddCustomTokenForm(model: AddCustomTokenForm) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius8),
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
elevation = TangemTheme.dimens.elevation4,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(TangemTheme.dimens.spacing16),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
if (model.showTokenFields) InputField(model = model.contractAddressInputField)
|
||||
SelectorField(model = model.networkSelectorField)
|
||||
if (model.showTokenFields) InputField(model = model.tokenNameInputField)
|
||||
if (model.showTokenFields) InputField(model = model.tokenSymbolInputField)
|
||||
if (model.showTokenFields) InputField(model = model.decimalsInputField)
|
||||
model.derivationPathSelectorField?.let { SelectorField(model = it) }
|
||||
if (model.derivationPathInputField?.showField == true) InputField(model = model.derivationPathInputField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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
|
||||
is AddCustomTokenInputField.DerivationPath -> true
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
value = model.value,
|
||||
onValueChange = model.onValueChange,
|
||||
keyboardOptions = model.keyboardOptions,
|
||||
label = {
|
||||
Text(
|
||||
text = model.label.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTextFieldsDefault.defaultTextFieldColors.labelColor(
|
||||
enabled = isEnabled,
|
||||
error = isError,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
).value,
|
||||
)
|
||||
},
|
||||
placeholder = {
|
||||
Text(
|
||||
text = model.placeholder.resolveReference(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTextFieldsDefault.defaultTextFieldColors
|
||||
.placeholderColor(enabled = isEnabled)
|
||||
.value,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
singleLine = true,
|
||||
enabled = isEnabled,
|
||||
isError = isError,
|
||||
colors = TangemTextFieldsDefault.defaultTextFieldColors,
|
||||
)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = (model as? AddCustomTokenInputField.ContactAddress)?.isLoading ?: false,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomCenter)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing6)
|
||||
.padding(bottom = TangemTheme.dimens.spacing6),
|
||||
) {
|
||||
LinearProgressIndicator(color = TangemTheme.colors.icon.primary1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Be careful with this function – ExposedDropdownMenuBox can crash the app if it is open and user clicks system back
|
||||
* button. It was fixed in compose-material 1.6.4.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun SelectorField(model: AddCustomTokenSelectorField) {
|
||||
var isExpanded by remember { mutableStateOf(value = false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
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
|
||||
is AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle -> item.subtitle
|
||||
}.resolveReference(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
enabled = isEnabled,
|
||||
label = { Text(text = model.label.resolveReference()) },
|
||||
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded) },
|
||||
colors = TangemTextFieldsDefault.defaultTextFieldColors,
|
||||
)
|
||||
|
||||
ExposedDropdownMenu(
|
||||
expanded = isExpanded && isEnabled,
|
||||
onDismissRequest = { isExpanded = false },
|
||||
) {
|
||||
model.items.forEachIndexed { index, item ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
model.onMenuItemClick(index)
|
||||
isExpanded = false
|
||||
},
|
||||
) {
|
||||
Column {
|
||||
Text(text = item.title.resolveReference())
|
||||
|
||||
val subtitle = (item as? AddCustomTokenSelectorField.SelectorItem.TitleWithSubtitle)
|
||||
?.subtitle?.resolveReference()
|
||||
|
||||
if (!subtitle.isNullOrBlank()) {
|
||||
Text(
|
||||
text = subtitle,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.caption2,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenForm(@PreviewParameter(AddCustomTokenFormProvider::class) model: AddCustomTokenForm) {
|
||||
TangemThemePreview {
|
||||
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))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.details.ui.cardsettings.TextReference
|
||||
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
|
||||
|
||||
/**
|
||||
* Add custom token toolbar
|
||||
*
|
||||
* @param title title
|
||||
* @param onBackButtonClick lambda be invoked when BackButton is been pressed
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun AddCustomTokenToolbar(title: TextReference, onBackButtonClick: () -> Unit) {
|
||||
val toolbarElevation = if (isSystemInDarkTheme()) {
|
||||
TangemTheme.dimens.elevation0
|
||||
} else {
|
||||
AppBarDefaults.TopAppBarElevation
|
||||
}
|
||||
TopAppBar(
|
||||
backgroundColor = TangemTheme.colors.background.primary,
|
||||
elevation = toolbarElevation,
|
||||
) {
|
||||
IconButton(onClick = onBackButtonClick) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_back_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing26))
|
||||
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
internal fun Preview_AddCustomTokenToolbar() {
|
||||
TangemThemePreview {
|
||||
AddCustomTokenToolbar(title = TextReference.Res(R.string.add_custom_token_title), onBackButtonClick = {})
|
||||
}
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.ui.components
|
||||
|
||||
import android.content.res.Configuration
|
||||
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.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.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
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 warnings
|
||||
*
|
||||
* @param warnings warnings descriptions set
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
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.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),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.common_warning),
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2.copy(fontWeight = FontWeight.Bold),
|
||||
)
|
||||
Text(
|
||||
text = warning.description.resolveReference(),
|
||||
fontSize = 13.sp,
|
||||
lineHeight = 18.sp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_AddCustomTokenWarnings() {
|
||||
TangemThemePreview {
|
||||
AddCustomTokenWarnings(warnings = AddCustomTokenPreviewData.createWarnings())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.validators
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.address.AddressService
|
||||
import com.tangem.common.card.EllipticCurve
|
||||
import com.tangem.domain.tokens.error.AddCustomTokenError
|
||||
|
||||
/**
|
||||
* Validator of contract address
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object ContractAddressValidator {
|
||||
|
||||
/** Validate a [address] using [blockchain] */
|
||||
fun validate(address: String, blockchain: Blockchain): ContractAddressValidatorResult {
|
||||
return when {
|
||||
address.isEmpty() -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.FIELD_IS_EMPTY)
|
||||
validateAddress(blockchain, address) -> ContractAddressValidatorResult.Success
|
||||
else -> ContractAddressValidatorResult.Error(type = AddCustomTokenError.INVALID_CONTRACT_ADDRESS)
|
||||
}
|
||||
}
|
||||
|
||||
private fun validateAddress(blockchain: Blockchain, address: String): Boolean {
|
||||
return when (blockchain) {
|
||||
Blockchain.Unknown,
|
||||
Blockchain.Binance,
|
||||
Blockchain.BinanceTestnet,
|
||||
-> SuccessAddressValidator.validate(address)
|
||||
Blockchain.Cardano -> blockchain.validateContractAddress(address)
|
||||
else -> blockchain.validateAddress(address)
|
||||
}
|
||||
}
|
||||
|
||||
private object SuccessAddressValidator : AddressService() {
|
||||
override fun makeAddress(walletPublicKey: ByteArray, curve: EllipticCurve?): String {
|
||||
throw UnsupportedOperationException()
|
||||
}
|
||||
|
||||
override fun validate(address: String): Boolean = true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.tap.features.customtoken.impl.presentation.validators
|
||||
|
||||
import com.tangem.domain.tokens.error.AddCustomTokenError
|
||||
|
||||
/**
|
||||
* Result of validation contract address
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed interface ContractAddressValidatorResult {
|
||||
|
||||
/** Success */
|
||||
object Success : ContractAddressValidatorResult
|
||||
|
||||
/**
|
||||
* Error
|
||||
*
|
||||
* @property type type of error
|
||||
*/
|
||||
data class Error(val type: AddCustomTokenError) : ContractAddressValidatorResult
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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))
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.tap.features.details.redux
|
||||
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
|
|
@ -14,15 +13,6 @@ sealed class DetailsAction : Action {
|
|||
val shouldSaveUserWallets: Boolean,
|
||||
) : DetailsAction()
|
||||
|
||||
data object ScanAndSaveUserWallet : DetailsAction() {
|
||||
|
||||
data object Success : DetailsAction()
|
||||
|
||||
data class Error(val error: TextReference?) : DetailsAction()
|
||||
}
|
||||
|
||||
data object DismissError : DetailsAction()
|
||||
|
||||
sealed class AppSettings : DetailsAction() {
|
||||
data class SwitchPrivacySetting(
|
||||
val enable: Boolean,
|
||||
|
|
|
|||
|
|
@ -2,36 +2,24 @@ package com.tangem.tap.features.details.redux
|
|||
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.core.TangemError
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.common.routing.utils.popTo
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListError
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
|
@ -41,7 +29,6 @@ import kotlinx.coroutines.launch
|
|||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
import timber.log.Timber
|
||||
import com.tangem.core.analytics.models.AnalyticsParam as CoreAnalyticsParam
|
||||
|
||||
class DetailsMiddleware {
|
||||
private val appSettingsMiddleware = AppSettingsMiddleware()
|
||||
|
|
@ -62,7 +49,6 @@ class DetailsMiddleware {
|
|||
private fun handleAction(state: DetailsState, action: Action) {
|
||||
when (action) {
|
||||
is DetailsAction.AppSettings -> appSettingsMiddleware.handle(state, action)
|
||||
is DetailsAction.ScanAndSaveUserWallet -> scanAndSaveUserWallet()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -278,95 +264,4 @@ class DetailsMiddleware {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun scanAndSaveUserWallet() = scope.launch(Dispatchers.IO) {
|
||||
val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository)
|
||||
|
||||
val prevUseBiometricsForAccessCode = cardSdkConfigRepository.isBiometricsRequestPolicy()
|
||||
|
||||
// Update access code policy for access code saving when a card was scanned
|
||||
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(isBiometricsRequestPolicy = shouldSaveAccessCodes)
|
||||
|
||||
store.inject(DaggerGraphState::scanCardProcessor).scan(
|
||||
analyticsSource = CoreAnalyticsParam.ScreensSources.Settings,
|
||||
onWalletNotCreated = {
|
||||
// No need to rollback policy, continue with the policy set before the card scan
|
||||
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
|
||||
},
|
||||
disclaimerWillShow = {
|
||||
store.dispatchNavigationAction(AppRouter::pop)
|
||||
},
|
||||
onSuccess = { scanResponse ->
|
||||
createUserWallet(scanResponse)
|
||||
.doOnSuccess {
|
||||
saveUserWalletAndPopBackToWalletScreen(
|
||||
userWallet = it,
|
||||
prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode,
|
||||
)
|
||||
}
|
||||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to create user wallet")
|
||||
handleError(error = error, prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode)
|
||||
}
|
||||
},
|
||||
onFailure = { error ->
|
||||
Timber.e(error, "Unable to scan card")
|
||||
handleError(error = error, prevUseBiometricsForAccessCode = prevUseBiometricsForAccessCode)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse): CompletionResult<UserWallet> {
|
||||
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
|
||||
val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase).build()
|
||||
|
||||
return if (userWallet != null) {
|
||||
CompletionResult.Success(userWallet)
|
||||
} else {
|
||||
CompletionResult.Failure(TangemSdkError.WalletIsNotCreated())
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun saveUserWalletAndPopBackToWalletScreen(
|
||||
userWallet: UserWallet,
|
||||
prevUseBiometricsForAccessCode: Boolean,
|
||||
) {
|
||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
|
||||
userWalletsListManager.save(userWallet)
|
||||
.doOnSuccess {
|
||||
store.onUserWalletSelected(userWallet)
|
||||
|
||||
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
|
||||
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
|
||||
}
|
||||
.doOnFailure { error ->
|
||||
if (error is UserWalletsListError.WalletAlreadySaved) {
|
||||
userWalletsListManager.select(userWallet.walletId)
|
||||
store.onUserWalletSelected(userWallet)
|
||||
|
||||
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Success)
|
||||
store.dispatchNavigationAction { popTo<AppRoute.Wallet>() }
|
||||
} else {
|
||||
Timber.e(error, "Unable to create user wallet")
|
||||
handleError(error, prevUseBiometricsForAccessCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun handleError(error: TangemError, prevUseBiometricsForAccessCode: Boolean) {
|
||||
val cardSdkConfigRepository = store.inject(DaggerGraphState::cardSdkConfigRepository)
|
||||
|
||||
// Rollback policy if card saving was failed
|
||||
cardSdkConfigRepository.setAccessCodeRequestPolicy(prevUseBiometricsForAccessCode)
|
||||
|
||||
store.dispatchWithMain(DetailsAction.ScanAndSaveUserWallet.Error(error.toTextReference()))
|
||||
}
|
||||
|
||||
private fun TangemError.toTextReference(): TextReference? {
|
||||
if (silent) return null
|
||||
|
||||
return messageResId?.let(::resourceReference) ?: stringReference(customMessage)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
package com.tangem.tap.features.details.redux
|
||||
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.CardDTO
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -21,7 +20,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
|
|||
val detailsState = state.detailsState
|
||||
return when (action) {
|
||||
is DetailsAction.PrepareScreen -> {
|
||||
handlePrepareScreen(action, state)
|
||||
handlePrepareScreen(action)
|
||||
}
|
||||
is DetailsAction.AppSettings -> {
|
||||
handlePrivacyAction(action, detailsState)
|
||||
|
|
@ -31,26 +30,12 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
|
|||
selectedAppCurrency = action.currency,
|
||||
),
|
||||
)
|
||||
is DetailsAction.ScanAndSaveUserWallet -> detailsState.copy(
|
||||
isScanningInProgress = true,
|
||||
)
|
||||
is DetailsAction.ScanAndSaveUserWallet.Error -> detailsState.copy(
|
||||
isScanningInProgress = false,
|
||||
error = action.error,
|
||||
)
|
||||
is DetailsAction.ScanAndSaveUserWallet.Success -> detailsState.copy(
|
||||
isScanningInProgress = false,
|
||||
)
|
||||
is DetailsAction.DismissError -> detailsState.copy(
|
||||
error = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: AppState): DetailsState {
|
||||
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen): DetailsState {
|
||||
return DetailsState(
|
||||
scanResponse = action.scanResponse,
|
||||
createBackupAllowed = action.scanResponse.card.backupStatus == CardDTO.BackupStatus.NoBackup,
|
||||
appSettingsState = AppSettingsState(
|
||||
isBiometricsAvailable = runBlocking {
|
||||
tangemSdkManager.checkCanUseBiometry()
|
||||
|
|
|
|||
|
|
@ -1,27 +1,16 @@
|
|||
package com.tangem.tap.features.details.redux
|
||||
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.appcurrency.model.AppCurrency
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class DetailsState(
|
||||
@Deprecated("Delete after onboarding refactoring")
|
||||
val scanResponse: ScanResponse? = null,
|
||||
val privacyPolicyUrl: String? = null,
|
||||
val createBackupAllowed: Boolean = false,
|
||||
val isScanningInProgress: Boolean = false,
|
||||
val error: TextReference? = null,
|
||||
val appSettingsState: AppSettingsState = AppSettingsState(),
|
||||
) : StateType
|
||||
|
||||
sealed class ResetCardDialog {
|
||||
data object StartResetDialog : ResetCardDialog()
|
||||
data object ContinueResetDialog : ResetCardDialog()
|
||||
data object InterruptedResetDialog : ResetCardDialog()
|
||||
data object CompletedResetDialog : ResetCardDialog()
|
||||
}
|
||||
|
||||
data class AppSettingsState(
|
||||
val saveWallets: Boolean = false,
|
||||
val saveAccessCodes: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import com.tangem.domain.balancehiding.repositories.BalanceHidingRepository
|
|||
import com.tangem.domain.settings.CanUseBiometryUseCase
|
||||
import com.tangem.domain.settings.repositories.SettingsRepository
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.features.details.DetailsFeatureToggles
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
|
|
@ -45,7 +44,6 @@ internal class AppSettingsViewModel @Inject constructor(
|
|||
private val appThemeModeRepository: AppThemeModeRepository,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val appSettingsItemsAnalyticsSender: AppSettingsItemsAnalyticsSender,
|
||||
private val detailsFeatureToggles: DetailsFeatureToggles,
|
||||
) : ViewModel(),
|
||||
StoreSubscriber<DetailsState>,
|
||||
DefaultLifecycleObserver {
|
||||
|
|
@ -62,9 +60,7 @@ internal class AppSettingsViewModel @Inject constructor(
|
|||
|
||||
init {
|
||||
bootstrapAppCurrencyUpdates()
|
||||
if (detailsFeatureToggles.isRedesignEnabled) {
|
||||
bootstrapBiometricsUpdates()
|
||||
}
|
||||
bootstrapBiometricsUpdates()
|
||||
|
||||
subscribeToStoreChanges()
|
||||
sendItemsAnalytics()
|
||||
|
|
|
|||
|
|
@ -1,71 +0,0 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.store
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
internal class DetailsFragment : ComposeFragment(), StoreSubscriber<DetailsState> {
|
||||
|
||||
@Inject
|
||||
override lateinit var uiDependencies: UiDependencies
|
||||
|
||||
@Inject
|
||||
lateinit var walletsRepository: WalletsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var userWalletsListManager: UserWalletsListManager
|
||||
|
||||
private lateinit var detailsViewModel: DetailsViewModel
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
detailsViewModel = DetailsViewModel(
|
||||
store = store,
|
||||
walletsRepository = walletsRepository,
|
||||
userWalletsListManager = userWalletsListManager,
|
||||
)
|
||||
Analytics.send(Settings.ScreenOpened())
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
DetailsScreen(
|
||||
modifier = modifier,
|
||||
state = detailsViewModel.detailsScreenState.value,
|
||||
onBackClick = { store.dispatchNavigationAction(AppRouter::pop) },
|
||||
)
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
store.subscribe(this) { state ->
|
||||
state.skipRepeats { oldState, newState ->
|
||||
oldState.detailsState == newState.detailsState
|
||||
}.select { it.detailsState }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun newState(state: DetailsState) {
|
||||
if (activity == null || view == null) return
|
||||
detailsViewModel.detailsScreenState.value = detailsViewModel.updateState(state)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,288 +0,0 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
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.SpacerH
|
||||
import com.tangem.core.ui.components.SpacerHMax
|
||||
import com.tangem.core.ui.event.EventEffect
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemColorPalette
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.common.ScreenTitle
|
||||
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
|
||||
@Composable
|
||||
internal fun DetailsScreen(state: DetailsScreenState, onBackClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
SettingsScreensScaffold(
|
||||
modifier = modifier,
|
||||
snackbarHostState = snackbarHostState,
|
||||
content = { Content(state = state) },
|
||||
onBackClick = onBackClick,
|
||||
)
|
||||
|
||||
ShowSnackbarIfNeeded(
|
||||
snackbarHostState = snackbarHostState,
|
||||
messageEvent = state.showSnackbar,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Content(state: DetailsScreenState, modifier: Modifier = Modifier) {
|
||||
Box(modifier = modifier) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
ScreenTitle(titleRes = R.string.details_title)
|
||||
SpacerH(height = TangemTheme.dimens.spacing36)
|
||||
SettingsItems(
|
||||
items = state.elements,
|
||||
)
|
||||
SpacerHMax()
|
||||
TangemSocialAccounts(
|
||||
links = state.tangemLinks,
|
||||
onSocialNetworkClick = state.onSocialNetworkClick,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing12)
|
||||
TangemAppVersion(
|
||||
appNameRes = state.appNameRes,
|
||||
version = state.tangemVersion,
|
||||
)
|
||||
SpacerH(height = TangemTheme.dimens.spacing16)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsItems(items: List<SettingsItem>) {
|
||||
items.forEach { item ->
|
||||
if (item.isLarge) {
|
||||
LargeDetailsItem(item)
|
||||
} else {
|
||||
DetailsItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LargeDetailsItem(item: SettingsItem) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(onClick = item.onClick)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing20)
|
||||
.heightIn(min = TangemTheme.dimens.size84)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (item.showProgress) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = item.iconResId),
|
||||
contentDescription = item.title.resolveReference(),
|
||||
tint = TangemColorPalette.Azure,
|
||||
)
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.heightIn(min = TangemTheme.dimens.size56),
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(
|
||||
space = TangemTheme.dimens.spacing4,
|
||||
alignment = Alignment.CenterVertically,
|
||||
),
|
||||
) {
|
||||
Text(
|
||||
text = item.title.resolveReference(),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
if (item.subtitle != null) {
|
||||
Text(
|
||||
text = item.subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.body1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DetailsItem(item: SettingsItem) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = !item.showProgress, onClick = item.onClick)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing20)
|
||||
.heightIn(min = TangemTheme.dimens.size56)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing20),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
if (item.showProgress) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
color = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = item.iconResId),
|
||||
contentDescription = item.title.resolveReference(),
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.Start,
|
||||
verticalArrangement = Arrangement.SpaceAround,
|
||||
) {
|
||||
Text(
|
||||
text = item.title.resolveReference(),
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
|
||||
if (item.subtitle != null) {
|
||||
Text(
|
||||
text = item.subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
items(links) {
|
||||
val onClick = remember(it) {
|
||||
{ onSocialNetworkClick(it) }
|
||||
}
|
||||
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = TangemTheme.dimens.spacing4)
|
||||
.size(TangemTheme.dimens.size32),
|
||||
onClick = onClick,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size24),
|
||||
painter = painterResource(id = it.network.iconRes),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShowSnackbarIfNeeded(snackbarHostState: SnackbarHostState, messageEvent: StateEvent<TextReference>) {
|
||||
var message: TextReference? by remember { mutableStateOf(value = null) }
|
||||
val resolvedMessage by rememberUpdatedState(newValue = message?.resolveReference())
|
||||
|
||||
LaunchedEffect(resolvedMessage) {
|
||||
resolvedMessage?.let {
|
||||
snackbarHostState.showSnackbar(it)
|
||||
}
|
||||
}
|
||||
|
||||
EventEffect(messageEvent) {
|
||||
message = it
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TangemAppVersion(appNameRes: Int, version: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing16),
|
||||
text = "${stringResource(id = appNameRes)} $version",
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
}
|
||||
|
||||
// region Preview
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 900)
|
||||
@Preview(showBackground = true, widthDp = 360, heightDp = 900, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun DetailsScreenPreview(@PreviewParameter(DetailsScreenStateProvider::class) param: DetailsScreenState) {
|
||||
TangemThemePreview {
|
||||
DetailsScreen(param, onBackClick = {})
|
||||
}
|
||||
}
|
||||
|
||||
private class DetailsScreenStateProvider : CollectionPreviewParameterProvider<DetailsScreenState>(
|
||||
collection = buildList {
|
||||
DetailsScreenState(
|
||||
elements = buildList {
|
||||
SettingsItem.WalletConnect({}).let(::add)
|
||||
SettingsItem.AddWallet(showProgress = false, {}).let(::add)
|
||||
SettingsItem.LinkMoreCards({}).let(::add)
|
||||
SettingsItem.CardSettings({}).let(::add)
|
||||
SettingsItem.AppSettings({}).let(::add)
|
||||
// removed chat in task [REDACTED_TASK_KEY]
|
||||
// SettingsItem.Chat({}).let(::add)
|
||||
SettingsItem.SendFeedback({}).let(::add)
|
||||
SettingsItem.ReferralProgram({}).let(::add)
|
||||
SettingsItem.TermsOfService({}).let(::add)
|
||||
}.toImmutableList(),
|
||||
tangemLinks = TangemSocialAccounts.accountsEn,
|
||||
tangemVersion = "Tangem 2.14.12 (343)",
|
||||
showSnackbar = consumedEvent(),
|
||||
onSocialNetworkClick = {},
|
||||
).let(::add)
|
||||
|
||||
DetailsScreenState(
|
||||
elements = buildList {
|
||||
SettingsItem.WalletConnect({}).let(::add)
|
||||
SettingsItem.AddWallet(showProgress = true, {}).let(::add)
|
||||
SettingsItem.CardSettings({}).let(::add)
|
||||
SettingsItem.AppSettings({}).let(::add)
|
||||
// removed chat in task [REDACTED_TASK_KEY]
|
||||
// SettingsItem.Chat({}).let(::add)
|
||||
SettingsItem.SendFeedback({}).let(::add)
|
||||
SettingsItem.TermsOfService({}).let(::add)
|
||||
}.toImmutableList(),
|
||||
tangemLinks = TangemSocialAccounts.accountsRu,
|
||||
tangemVersion = "Tangem 2.14.12 (343)",
|
||||
showSnackbar = consumedEvent(),
|
||||
onSocialNetworkClick = {},
|
||||
).let(::add)
|
||||
},
|
||||
)
|
||||
// endregion Preview
|
||||
|
|
@ -1,162 +0,0 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resourceReference
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
@Immutable
|
||||
internal data class DetailsScreenState(
|
||||
val elements: ImmutableList<SettingsItem>,
|
||||
val tangemLinks: ImmutableList<SocialNetworkLink>,
|
||||
val tangemVersion: String,
|
||||
val showSnackbar: StateEvent<TextReference>,
|
||||
val onSocialNetworkClick: (SocialNetworkLink) -> Unit,
|
||||
) {
|
||||
val appNameRes: Int = R.string.tangem_app_name
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal sealed class SettingsItem(
|
||||
@DrawableRes val iconResId: Int,
|
||||
val title: TextReference,
|
||||
val subtitle: TextReference? = null,
|
||||
val isLarge: Boolean = false,
|
||||
) {
|
||||
|
||||
abstract val onClick: () -> Unit
|
||||
|
||||
open val showProgress: Boolean = false
|
||||
|
||||
data class WalletConnect(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_wallet_connect_24,
|
||||
title = resourceReference(R.string.wallet_connect_title),
|
||||
subtitle = resourceReference(R.string.wallet_connect_subtitle),
|
||||
isLarge = true,
|
||||
)
|
||||
|
||||
data class AddWallet(
|
||||
override val showProgress: Boolean,
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
title = resourceReference(R.string.user_wallet_list_add_button),
|
||||
)
|
||||
|
||||
data class ScanWallet(
|
||||
override val showProgress: Boolean,
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_plus_24,
|
||||
title = resourceReference(R.string.scan_card_settings_button),
|
||||
)
|
||||
|
||||
data class LinkMoreCards(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_more_cards_24,
|
||||
title = resourceReference(R.string.details_row_title_create_backup),
|
||||
)
|
||||
|
||||
data class CardSettings(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_card_settings_24,
|
||||
title = resourceReference(R.string.card_settings_title),
|
||||
)
|
||||
|
||||
data class AppSettings(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_settings,
|
||||
title = resourceReference(R.string.app_settings_title),
|
||||
)
|
||||
|
||||
data class Chat(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_chat_24,
|
||||
title = resourceReference(R.string.details_chat),
|
||||
)
|
||||
|
||||
data class SendFeedback(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_comment_24,
|
||||
title = resourceReference(R.string.details_row_title_contact_to_support),
|
||||
)
|
||||
|
||||
data class ReferralProgram(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_add_friends_24,
|
||||
title = resourceReference(R.string.details_referral_title),
|
||||
)
|
||||
|
||||
data class TermsOfService(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_text_24,
|
||||
title = resourceReference(R.string.disclaimer_title),
|
||||
)
|
||||
|
||||
data class TesterMenu(
|
||||
override val onClick: () -> Unit,
|
||||
) : SettingsItem(
|
||||
iconResId = R.drawable.ic_alert_24,
|
||||
title = resourceReference(R.string.tester_menu),
|
||||
)
|
||||
}
|
||||
|
||||
@Immutable
|
||||
internal data class SocialNetworkLink(
|
||||
val network: SocialNetwork,
|
||||
val url: String,
|
||||
)
|
||||
|
||||
internal sealed class EventError {
|
||||
object Empty : EventError()
|
||||
}
|
||||
|
||||
sealed class SocialNetwork(val id: String, val iconRes: Int) {
|
||||
object Twitter : SocialNetwork("Twitter", R.drawable.ic_twitter_24)
|
||||
object Telegram : SocialNetwork("Telegram", R.drawable.ic_telegram_24)
|
||||
object Discord : SocialNetwork("Discord", R.drawable.ic_discord_24)
|
||||
object Reddit : SocialNetwork("Reddit", R.drawable.ic_reddit_24)
|
||||
object Instagram : SocialNetwork("Instagram", R.drawable.ic_instagram_24)
|
||||
object GitHub : SocialNetwork("GitHub", R.drawable.ic_github_24)
|
||||
object Facebook : SocialNetwork("Facebook", R.drawable.ic_facebook_24)
|
||||
object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin_24)
|
||||
object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube_24)
|
||||
}
|
||||
|
||||
internal object TangemSocialAccounts {
|
||||
val accountsEn: ImmutableList<SocialNetworkLink> = persistentListOf(
|
||||
SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"),
|
||||
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"),
|
||||
SocialNetworkLink(SocialNetwork.Instagram, "https://www.instagram.com/tangemwallet"),
|
||||
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Facebook, "https://www.facebook.com/tangemwallet"),
|
||||
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem_official"),
|
||||
)
|
||||
val accountsRu: ImmutableList<SocialNetworkLink> = persistentListOf(
|
||||
SocialNetworkLink(SocialNetwork.Twitter, "https://x.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat_ru"),
|
||||
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Reddit, "https://www.reddit.com/r/Tangem/"),
|
||||
SocialNetworkLink(SocialNetwork.Instagram, "https://www.instagram.com/tangemwallet"),
|
||||
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.Facebook, "https://www.facebook.com/tangemwallet"),
|
||||
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
|
||||
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/@tangem_official"),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,215 +0,0 @@
|
|||
package com.tangem.tap.features.details.ui.details
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
|
||||
import com.tangem.core.ui.event.StateEvent
|
||||
import com.tangem.core.ui.event.consumedEvent
|
||||
import com.tangem.core.ui.event.triggeredEvent
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.repository.WalletsRepository
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.feedback.FeedbackEmail
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.features.details.redux.DetailsAction
|
||||
import com.tangem.tap.features.details.redux.DetailsState
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
|
||||
import com.tangem.tap.features.home.LocaleRegionProvider
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.wallet.BuildConfig
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.rekotlin.Store
|
||||
import timber.log.Timber
|
||||
|
||||
internal class DetailsViewModel(
|
||||
private val store: Store<AppState>,
|
||||
private val walletsRepository: WalletsRepository,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
) {
|
||||
|
||||
var detailsScreenState: MutableState<DetailsScreenState> = mutableStateOf(updateState(store.state.detailsState))
|
||||
private set
|
||||
|
||||
init {
|
||||
bootstrapScreenState()
|
||||
}
|
||||
|
||||
fun updateState(state: DetailsState): DetailsScreenState {
|
||||
return DetailsScreenState(
|
||||
elements = createSettingsItems(state),
|
||||
tangemLinks = getSocialLinks(),
|
||||
tangemVersion = getTangemAppVersion(),
|
||||
showSnackbar = triggerErrorSnackbarIfNeeded(state.error),
|
||||
onSocialNetworkClick = ::handleSocialNetworkClick,
|
||||
)
|
||||
}
|
||||
|
||||
private fun createSettingsItems(state: DetailsState): ImmutableList<SettingsItem> {
|
||||
val scanResponse = state.scanResponse ?: return persistentListOf()
|
||||
val cardTypesResolver = scanResponse.cardTypesResolver
|
||||
|
||||
return buildList {
|
||||
SettingsItem.WalletConnect(::navigateToWalletConnect)
|
||||
.takeIf { cardTypesResolver.isMultiwalletAllowed() }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.AddWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet)
|
||||
.takeIf { state.appSettingsState.saveWallets }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.ScanWallet(showProgress = state.isScanningInProgress, ::scanAndSaveUserWallet)
|
||||
.takeUnless { state.appSettingsState.saveWallets }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.LinkMoreCards(::linkMoreCards)
|
||||
.takeIf { state.createBackupAllowed }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.CardSettings(::navigateToCardSettings)
|
||||
.let(::add)
|
||||
|
||||
SettingsItem.AppSettings(::navigateToAppSettings)
|
||||
.let(::add)
|
||||
|
||||
SettingsItem.SendFeedback(::sendFeedback)
|
||||
.let(::add)
|
||||
|
||||
SettingsItem.ReferralProgram(::navigateToReferralProgram)
|
||||
.takeIf { cardTypesResolver.isTangemWallet() }
|
||||
?.let(::add)
|
||||
|
||||
SettingsItem.TermsOfService(::navigateToToS)
|
||||
.let(::add)
|
||||
|
||||
SettingsItem.TesterMenu(::navigateToTesterMenu)
|
||||
.takeIf { BuildConfig.TESTER_MENU_ENABLED }
|
||||
?.let(::add)
|
||||
}.toImmutableList()
|
||||
}
|
||||
|
||||
private fun triggerErrorSnackbarIfNeeded(text: TextReference?): StateEvent<TextReference> {
|
||||
return if (text == null) {
|
||||
consumedEvent()
|
||||
} else {
|
||||
triggeredEvent(text) {
|
||||
store.dispatch(DetailsAction.DismissError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTangemAppVersion(): String {
|
||||
val versionCode: Int = BuildConfig.VERSION_CODE
|
||||
val versionName: String = BuildConfig.VERSION_NAME
|
||||
return "$versionName ($versionCode)"
|
||||
}
|
||||
|
||||
private fun navigateToTesterMenu() {
|
||||
store.dispatchNavigationAction {
|
||||
push(AppRoute.TesterMenu)
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateToToS() {
|
||||
store.dispatchOnMain(DisclaimerAction.Show(DisclaimerSource.Details))
|
||||
}
|
||||
|
||||
private fun navigateToReferralProgram() {
|
||||
val userWallet = userWalletsListManager.selectedUserWalletSync
|
||||
?: error("Selected wallet must be not null")
|
||||
|
||||
store.dispatchNavigationAction { push(AppRoute.ReferralProgram(userWallet.walletId)) }
|
||||
}
|
||||
|
||||
private fun sendFeedback() {
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Settings))
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.SendEmail(
|
||||
feedbackData = FeedbackEmail(),
|
||||
scanResponse = userWalletsListManager.selectedUserWalletSync?.scanResponse
|
||||
?: error("ScanResponse must be not null"),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private fun navigateToAppSettings() {
|
||||
Analytics.send(Settings.ButtonAppSettings())
|
||||
store.dispatchNavigationAction { push(AppRoute.AppSettings) }
|
||||
}
|
||||
|
||||
private fun navigateToCardSettings() {
|
||||
val userWalletId = userWalletsListManager.selectedUserWalletSync?.walletId
|
||||
?: error("UserWalletId must be not null")
|
||||
Analytics.send(Settings.ButtonCardSettings())
|
||||
store.dispatchNavigationAction { push(AppRoute.CardSettings(userWalletId)) }
|
||||
}
|
||||
|
||||
private fun linkMoreCards() {
|
||||
Analytics.send(Settings.ButtonCreateBackup())
|
||||
|
||||
val selectedUserWallet = userWalletsListManager.selectedUserWalletSync.guard {
|
||||
Timber.e("Unable to backup wallet, no user wallet selected")
|
||||
return
|
||||
}
|
||||
val scanResponse = selectedUserWallet.scanResponse
|
||||
Analytics.addContext(scanResponse)
|
||||
store.dispatch(GlobalAction.Onboarding.Start(scanResponse, canSkipBackup = false))
|
||||
store.dispatchNavigationAction { push(AppRoute.OnboardingWallet()) }
|
||||
}
|
||||
|
||||
private fun scanAndSaveUserWallet() {
|
||||
Analytics.send(Settings.ScanNewCard)
|
||||
store.dispatchOnMain(DetailsAction.ScanAndSaveUserWallet)
|
||||
}
|
||||
|
||||
private fun navigateToWalletConnect() {
|
||||
Analytics.send(Settings.ButtonWalletConnect())
|
||||
store.dispatchNavigationAction { push(AppRoute.WalletConnectSessions) }
|
||||
}
|
||||
|
||||
private fun handleSocialNetworkClick(link: SocialNetworkLink) {
|
||||
Analytics.send(Settings.ButtonSocialNetwork(link.network))
|
||||
store.dispatchOpenUrl(link.url)
|
||||
}
|
||||
|
||||
private fun getSocialLinks(): ImmutableList<SocialNetworkLink> {
|
||||
val locale = LocaleRegionProvider().getRegion()
|
||||
return if (locale.lowercase() == RUSSIA_COUNTRY_CODE) {
|
||||
TangemSocialAccounts.accountsRu
|
||||
} else {
|
||||
TangemSocialAccounts.accountsEn
|
||||
}
|
||||
}
|
||||
|
||||
private fun bootstrapScreenState() {
|
||||
userWalletsListManager.selectedUserWallet
|
||||
.distinctUntilChanged()
|
||||
.onEach { selectedUserWallet ->
|
||||
store.dispatchWithMain(
|
||||
DetailsAction.PrepareScreen(
|
||||
scanResponse = selectedUserWallet.scanResponse,
|
||||
shouldSaveUserWallets = walletsRepository.shouldSaveUserWalletsSync(),
|
||||
),
|
||||
)
|
||||
}
|
||||
.flowOn(Dispatchers.IO)
|
||||
.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.tangem.tap.features.details.ui.resetcard
|
||||
|
||||
internal sealed class ResetCardDialog {
|
||||
data object StartResetDialog : ResetCardDialog()
|
||||
data object ContinueResetDialog : ResetCardDialog()
|
||||
data object InterruptedResetDialog : ResetCardDialog()
|
||||
data object CompletedResetDialog : ResetCardDialog()
|
||||
}
|
||||
|
|
@ -22,7 +22,6 @@ import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
|
|||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.features.details.redux.ResetCardDialog
|
||||
import com.tangem.tap.features.details.ui.cardsettings.domain.CardSettingsInteractor
|
||||
import com.tangem.tap.features.details.ui.common.utils.getResetToFactoryDescription
|
||||
import com.tangem.tap.store
|
||||
|
|
|
|||
|
|
@ -24,12 +24,6 @@ abstract class BaseDisclaimer(
|
|||
override suspend fun isAccepted(): Boolean = dataProvider.isAccepted()
|
||||
}
|
||||
|
||||
class DummyDisclaimer : Disclaimer {
|
||||
override fun getUri(): Uri = Uri.parse("https://tangem.com/tangem_tos.html")
|
||||
override suspend fun accept() {}
|
||||
override suspend fun isAccepted(): Boolean = false
|
||||
}
|
||||
|
||||
class TangemDisclaimer(dataProvider: DisclaimerDataProvider) : BaseDisclaimer(dataProvider) {
|
||||
override fun getUri(): Uri = Uri.parse("$baseUrl/tangem_tos.html")
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.features.disclaimer.redux
|
||||
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
import com.tangem.tap.features.disclaimer.Disclaimer
|
||||
import org.rekotlin.Action
|
||||
|
||||
sealed class DisclaimerAction : Action {
|
||||
|
||||
data class SetDisclaimer(val disclaimer: Disclaimer) : DisclaimerAction()
|
||||
|
||||
data class Show(
|
||||
val from: DisclaimerSource,
|
||||
val callback: DisclaimerCallback? = null,
|
||||
) : DisclaimerAction()
|
||||
|
||||
object AcceptDisclaimer : DisclaimerAction()
|
||||
object OnBackPressed : DisclaimerAction()
|
||||
|
||||
data class OnProgressStateChanged(val state: ProgressState?) : DisclaimerAction()
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
package com.tangem.tap.features.disclaimer.redux
|
||||
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.Action
|
||||
import org.rekotlin.Middleware
|
||||
|
||||
class DisclaimerMiddleware {
|
||||
val disclaimerMiddleware: Middleware<AppState> = { dispatch, state ->
|
||||
{ next ->
|
||||
{ action ->
|
||||
state()?.let { handleDisclaimerMiddleware(action, it) }
|
||||
next(action)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleDisclaimerMiddleware(action: Action, appState: AppState) {
|
||||
val state = appState.disclaimerState
|
||||
|
||||
when (action) {
|
||||
is DisclaimerAction.Show -> {
|
||||
store.dispatchNavigationAction {
|
||||
push(AppRoute.Disclaimer(isTosAccepted = action.from == DisclaimerSource.Details))
|
||||
}
|
||||
}
|
||||
is DisclaimerAction.AcceptDisclaimer -> {
|
||||
mainScope.launch {
|
||||
state.disclaimer.accept()
|
||||
store.dispatchNavigationAction(AppRouter::pop)
|
||||
state.callback?.onAccept?.invoke()
|
||||
}
|
||||
}
|
||||
is DisclaimerAction.OnBackPressed -> {
|
||||
store.dispatchNavigationAction(AppRouter::pop)
|
||||
state.callback?.onDismiss?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package com.tangem.tap.features.disclaimer.redux
|
||||
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import org.rekotlin.Action
|
||||
|
||||
object DisclaimerReducer {
|
||||
fun reduce(action: Action, state: AppState): DisclaimerState = internalReduce(action, state)
|
||||
}
|
||||
|
||||
private fun internalReduce(action: Action, state: AppState): DisclaimerState {
|
||||
if (action !is DisclaimerAction) return state.disclaimerState
|
||||
|
||||
val disclaimerState = state.disclaimerState
|
||||
|
||||
return when (action) {
|
||||
is DisclaimerAction.SetDisclaimer -> disclaimerState.copy(
|
||||
disclaimer = action.disclaimer,
|
||||
)
|
||||
is DisclaimerAction.Show -> disclaimerState.copy(
|
||||
showedFrom = action.from,
|
||||
callback = action.callback,
|
||||
)
|
||||
is DisclaimerAction.AcceptDisclaimer, is DisclaimerAction.OnBackPressed -> disclaimerState.copy(
|
||||
callback = null,
|
||||
progressState = null,
|
||||
)
|
||||
is DisclaimerAction.OnProgressStateChanged -> disclaimerState.copy(
|
||||
progressState = action.state,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
package com.tangem.tap.features.disclaimer.redux
|
||||
|
||||
enum class DisclaimerSource {
|
||||
Home, Details
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.features.disclaimer.redux
|
||||
|
||||
import com.tangem.common.extensions.VoidCallback
|
||||
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
import com.tangem.tap.features.disclaimer.Disclaimer
|
||||
import com.tangem.tap.features.disclaimer.DummyDisclaimer
|
||||
import org.rekotlin.StateType
|
||||
|
||||
data class DisclaimerState(
|
||||
val disclaimer: Disclaimer = DummyDisclaimer(),
|
||||
val showedFrom: DisclaimerSource = DisclaimerSource.Home,
|
||||
val callback: DisclaimerCallback? = null,
|
||||
val progressState: ProgressState? = null,
|
||||
) : StateType
|
||||
|
||||
data class DisclaimerCallback(
|
||||
val onAccept: VoidCallback? = null,
|
||||
val onDismiss: VoidCallback? = null,
|
||||
)
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
package com.tangem.tap.features.disclaimer.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.View.OVER_SCROLL_NEVER
|
||||
import android.webkit.WebView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.transition.TransitionInflater
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
|
||||
import com.tangem.core.ui.extensions.setStatusBarColor
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
import com.tangem.tap.common.extensions.beginDelayedTransition
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.BaseFragment
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
import com.tangem.tap.features.disclaimer.Disclaimer
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerSource
|
||||
import com.tangem.tap.features.disclaimer.redux.DisclaimerState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.FragmentDisclaimerBinding
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.StoreSubscriber
|
||||
|
||||
class DisclaimerFragment : BaseFragment(R.layout.fragment_disclaimer), StoreSubscriber<DisclaimerState> {
|
||||
|
||||
private val binding: FragmentDisclaimerBinding by viewBinding(FragmentDisclaimerBinding::bind)
|
||||
private val webViewClient = DisclaimerWebViewClient()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
addBackPressHandler(handler = this)
|
||||
|
||||
binding.apply {
|
||||
toolbar.setNavigationOnClickListener { handleOnBackPressed() }
|
||||
webView.apply {
|
||||
settings.allowFileAccess = false
|
||||
settings.javaScriptEnabled = false
|
||||
overScrollMode = OVER_SCROLL_NEVER
|
||||
webViewClient = this@DisclaimerFragment.webViewClient
|
||||
}
|
||||
webView.hide()
|
||||
groupError.hide()
|
||||
groupAccept.hide()
|
||||
groupLoading.hide()
|
||||
|
||||
btnAccept.setOnClickListener {
|
||||
store.dispatch(DisclaimerAction.AcceptDisclaimer)
|
||||
}
|
||||
btnRepeat.setOnClickListener {
|
||||
webViewClient.reset()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
setStatusBarColor(R.color.background_secondary)
|
||||
|
||||
webViewClient.onProgressStateChanged = { store.dispatch(DisclaimerAction.OnProgressStateChanged(it)) }
|
||||
store.subscribe(subscriber = this) { state ->
|
||||
state
|
||||
.skipRepeats { oldState, newState -> oldState.disclaimerState == newState.disclaimerState }
|
||||
.select(AppState::disclaimerState)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
webViewClient.onProgressStateChanged = null
|
||||
store.unsubscribe(this)
|
||||
super.onStop()
|
||||
}
|
||||
|
||||
override fun configureTransitions() {
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
when (store.state.disclaimerState.showedFrom) {
|
||||
DisclaimerSource.Home -> {
|
||||
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
|
||||
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
|
||||
}
|
||||
DisclaimerSource.Details -> {
|
||||
super.configureTransitions()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun handleOnBackPressed() {
|
||||
store.dispatch(DisclaimerAction.OnBackPressed)
|
||||
}
|
||||
|
||||
override fun newState(state: DisclaimerState) {
|
||||
return with(binding) {
|
||||
if (activity == null || view == null) return
|
||||
|
||||
updateUiVisibility(state.disclaimer, state.progressState)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateUiVisibility(disclaimer: Disclaimer, progressState: ProgressState?) = with(binding) {
|
||||
when (progressState) {
|
||||
ProgressState.Loading -> {
|
||||
root.beginDelayedTransition()
|
||||
webView.hide()
|
||||
groupError.hide()
|
||||
groupAccept.hide()
|
||||
groupLoading.show()
|
||||
}
|
||||
ProgressState.Done -> {
|
||||
root.beginDelayedTransition()
|
||||
groupError.hide()
|
||||
groupLoading.hide()
|
||||
webView.show()
|
||||
lifecycleScope.launch {
|
||||
groupAccept.show(!disclaimer.isAccepted())
|
||||
}
|
||||
}
|
||||
ProgressState.Error -> {
|
||||
root.beginDelayedTransition()
|
||||
webView.show()
|
||||
groupAccept.show()
|
||||
groupLoading.hide()
|
||||
groupError.hide()
|
||||
webView.loadLocalTermsOfServices()
|
||||
}
|
||||
else -> {
|
||||
webView.setBackgroundColor(resources.getColor(R.color.transparent, null))
|
||||
webView.loadUrl(disclaimer.getUri().toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun WebView.loadLocalTermsOfServices() {
|
||||
loadData(localTermsOfServices, "text/html", "UTF-8")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
package com.tangem.tap.features.disclaimer.ui
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.webkit.*
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
|
||||
class DisclaimerWebViewClient : WebViewClient() {
|
||||
|
||||
var onProgressStateChanged: ((ProgressState) -> Unit)? = null
|
||||
|
||||
private var loadingUrl: String? = null
|
||||
private var loadedUrl: String? = null
|
||||
|
||||
private var progressState: ProgressState = ProgressState.Loading
|
||||
set(value) {
|
||||
field = value
|
||||
onProgressStateChanged?.invoke(value)
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
loadingUrl = null
|
||||
loadedUrl = null
|
||||
progressState = ProgressState.Loading
|
||||
}
|
||||
|
||||
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
|
||||
super.onPageStarted(view, url, favicon)
|
||||
|
||||
if (loadingUrl != url) progressState = ProgressState.Loading
|
||||
loadingUrl = url
|
||||
}
|
||||
|
||||
override fun onPageFinished(view: WebView?, url: String?) {
|
||||
super.onPageFinished(view, url)
|
||||
|
||||
if (loadedUrl != url) progressState = ProgressState.Done
|
||||
loadedUrl = url
|
||||
}
|
||||
|
||||
override fun onReceivedError(view: WebView?, resourceRequest: WebResourceRequest?, error: WebResourceError?) {
|
||||
super.onReceivedError(view, resourceRequest, error)
|
||||
error?.let { progressState = ProgressState.Error }
|
||||
}
|
||||
|
||||
override fun onReceivedHttpError(
|
||||
view: WebView?,
|
||||
resourceRequest: WebResourceRequest?,
|
||||
errorResponse: WebResourceResponse?,
|
||||
) {
|
||||
super.onReceivedHttpError(view, resourceRequest, errorResponse)
|
||||
|
||||
ifNotNull(resourceRequest, errorResponse) { request, response ->
|
||||
if (request.url?.toString() != loadingUrl || response.statusCode < RESPONSE_USER_ERROR_STATUS_CODE) return
|
||||
if (progressState != ProgressState.Done) return
|
||||
|
||||
progressState = ProgressState.Error
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RESPONSE_USER_ERROR_STATUS_CODE = 400
|
||||
}
|
||||
}
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
package com.tangem.tap.features.disclaimer.ui
|
||||
|
||||
internal val localTermsOfServices = """
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Legal Disclaimer</title>
|
||||
<style>
|
||||
@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Light.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Light.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Light.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Light.ttf) format("truetype");font-weight:300;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Regular.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Regular.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Regular.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Regular.ttf) format("truetype");font-weight:400;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Medium.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Medium.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Medium.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Medium.ttf) format("truetype");font-weight:500;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Semibold.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Semibold.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Semibold.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Semibold.ttf) format("truetype");font-weight:600;font-style:normal;font-display:swap}@font-face{font-family:"SF Pro Display";src:url(/fonts/sf-pro/SFProDisplay-Bold.eot) format("embedded-opentype"),url(/fonts/sf-pro/SFProDisplay-Bold.woff2) format("woff2"),url(/fonts/sf-pro/SFProDisplay-Bold.woff) format("woff"),url(/fonts/sf-pro/SFProDisplay-Bold.ttf) format("truetype");font-weight:700;font-style:normal;font-display:swap}*{margin:0;padding:0}body{padding:0;margin:1rem;color:rgb(0 0 0);font-family:'SF Pro Display',-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif}h1{font-weight:700;font-size:30px;line-height:1.2;margin-bottom:1rem}p{font-size:16px;line-height:20px;letter-spacing:-.24px;margin-bottom:1rem}
|
||||
</style>
|
||||
</head>
|
||||
<body><h1>Legal Disclaimer</h1>
|
||||
<p> 1. Tangem application (Software)</p>
|
||||
<p> The Software is intended for usage only with Tangem hardware wallets (Cards) via NFC interface. The
|
||||
Software DOES NOT:</p>
|
||||
<p> a) Generate, store, transmit, or have access to private (secret) cryptographic keys to blockchain
|
||||
wallets holding digital assets, including crypto-currency.</p>
|
||||
<p> b) Generate, store, transmit, or have access to secret keys, passwords, passphrases, recovery phrases
|
||||
that can be used to restore or to copy private (secret) keys to blockchain wallets holding digital assets, including
|
||||
crypto-currency.</p>
|
||||
<p> c) Provide exchange, trading, investment services on behalf of Tangem AG.</p>
|
||||
<p> 2. Risks related to the use of Software</p>
|
||||
<p> Tangem will not be responsible for any losses, damages or claims arising from events falling within the
|
||||
scope of the following five categories:</p>
|
||||
<p> a) Mistakes made by the user of any cryptocurrency-related software or service, e.g., forgotten
|
||||
passwords, payments sent to wrong addresses, and accidental deletion of blockchain wallets on Cards.</p>
|
||||
<p> b) Problems of Software and/or any blockchain- or cryptocurrency- related software or service, e.g.,
|
||||
corrupted files, incorrectly constructed transactions, unsafe cryptographic libraries, malware.</p>
|
||||
<p> c) Technical failures in the hardware of the user, including Cards, of any cryptocurrency-related
|
||||
software or service, e.g., data loss due to a faulty or damaged storage device.</p>
|
||||
<p> d) Security problems experienced by the user of any cryptocurrency-related software or service, e.g.,
|
||||
unauthorized access to users' wallets and/or accounts.</p>
|
||||
<p> e) Actions or inactions of third parties and/or events experienced by third parties, e.g., bankruptcy
|
||||
of service providers, information security attacks on service providers, and fraud conducted by third parties.</p>
|
||||
<p> 3. Trading and Investment risks</p>
|
||||
<p> There is considerable exposure to risk in any crypto-currency or other digital asset exchange
|
||||
transaction. Any transaction involving currencies involves risks including, but not limited to, the potential for
|
||||
changing economic conditions that may substantially affect the price or liquidity of a currency. Investments in
|
||||
crypto-currency exchange speculation may also be susceptible to sharp rises and falls as the relevant market values
|
||||
fluctuate. It is for this reason that when speculating in such markets it is advisable to use only risk capital.</p>
|
||||
<p> 4. Electronic Trading Risks</p>
|
||||
<p> Before you engage in transactions using an electronic system, you should carefully review the rules and
|
||||
regulations of the exchanges offering the system and/or listing the instruments you intend to trade. Online trading
|
||||
has inherent risk due to system response and access times that may vary due to market conditions, system
|
||||
performance, and other factors. You should understand these and additional risks before trading.</p>
|
||||
<p> 5. Compliance with tax obligations </p>
|
||||
<p> The users of the Software are solely responsible to determinate what, if any, taxes apply to their
|
||||
crypto-currency transactions. The owners of, or contributors to, the Software are NOT responsible for determining
|
||||
the taxes that apply to crypto-currency transactions. </p>
|
||||
<p> 6. No warranties </p>
|
||||
<p> The Software is provided on an "as is" basis without any warranties of any kind regarding the
|
||||
Software and/or any content, data, materials and/or services provided on the Software.
|
||||
</p>
|
||||
<p> 7. Limitation of liability</p>
|
||||
<p> Unless otherwise required by law, in no event shall the owners of, or contributors to, the Software be
|
||||
liable for any damages of any kind, including, but not limited to, loss of use, loss of profits, or loss of data
|
||||
arising out of or in any way connected with the use of the Software. In no way are the owners of, or contributors
|
||||
to, the Software responsible for the actions, decisions, or other behavior taken or not taken by you in reliance
|
||||
upon the Software.</p>
|
||||
<p> 8. Last amendment</p>
|
||||
<p> This disclaimer was amended for the last time on October 1st, 2020.</p></body>
|
||||
</html>
|
||||
""".trimIndent()
|
||||
|
|
@ -58,7 +58,9 @@ internal fun StoriesScreen(
|
|||
}
|
||||
|
||||
StoriesScreenContent(
|
||||
modifier = Modifier.fillMaxSize().testTag(TestTags.STORIES_SCREEN),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.testTag(TestTags.STORIES_SCREEN),
|
||||
config = StoriesScreenContentConfig(
|
||||
storiesSize = state.stories.lastIndex,
|
||||
currentStoryIndex = currentStoryIndex,
|
||||
|
|
@ -157,7 +159,7 @@ private fun StoriesScreenContent(config: StoriesScreenContentConfig, modifier: M
|
|||
duration = currentStoryDuration,
|
||||
)
|
||||
Stories.RevolutionaryWallet -> StoriesRevolutionaryWallet()
|
||||
is Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
|
||||
Stories.UltraSecureBackup -> StoriesUltraSecureBackup(
|
||||
isPaused = isPaused,
|
||||
stepDuration = currentStoryDuration,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.tap.features.home.featuretoggles
|
||||
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import javax.inject.Inject
|
||||
|
||||
class HomeFeatureToggles @Inject constructor(
|
||||
private val featureTogglesManager: FeatureTogglesManager,
|
||||
) {
|
||||
|
||||
val isMigrateUserCountryCodeEnabled: Boolean
|
||||
get() = featureTogglesManager.isFeatureEnabled(name = "MIGRATE_USER_COUNTRY_CODE_ENABLED")
|
||||
}
|
||||
|
|
@ -15,6 +15,4 @@ sealed class HomeAction : Action {
|
|||
data class ReadCard(val scope: CoroutineScope) : HomeAction()
|
||||
|
||||
data class ScanInProgress(val scanInProgress: Boolean) : HomeAction()
|
||||
|
||||
data class UpdateCountryCode(val userCountryCode: String) : HomeAction()
|
||||
}
|
||||
|
|
@ -10,16 +10,10 @@ object HomeReducer {
|
|||
private fun internalReduce(action: Action, appState: AppState): HomeState {
|
||||
if (action !is HomeAction) return appState.homeState
|
||||
|
||||
var state = appState.homeState
|
||||
when (action) {
|
||||
return when (action) {
|
||||
is HomeAction.ScanInProgress -> {
|
||||
state = state.copy(scanInProgress = action.scanInProgress)
|
||||
appState.homeState.copy(scanInProgress = action.scanInProgress)
|
||||
}
|
||||
is HomeAction.UpdateCountryCode -> {
|
||||
state.onCountryCodeUpdate(state, action.userCountryCode)
|
||||
}
|
||||
else -> {}
|
||||
else -> appState.homeState
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
|
|
@ -1,56 +1,24 @@
|
|||
package com.tangem.tap.features.home.redux
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import org.rekotlin.StateType
|
||||
import java.util.Locale
|
||||
|
||||
@Immutable
|
||||
data class HomeState(
|
||||
val scanInProgress: Boolean = false,
|
||||
val stories: List<Stories> = initDefaultStories(),
|
||||
val stories: ImmutableList<Stories> = Stories.entries.toImmutableList(),
|
||||
) : StateType {
|
||||
|
||||
val firstStory: Stories
|
||||
get() = stories[0]
|
||||
val firstStory: Stories get() = stories[0]
|
||||
|
||||
fun stepOf(story: Stories): Int = stories.indexOf(story)
|
||||
|
||||
fun onCountryCodeUpdate(homeState: HomeState, countryCode: String) {
|
||||
val isNewWalletAvailable = !(countryCode == RUSSIA_COUNTRY_CODE || countryCode == BELARUS_COUNTRY_CODE)
|
||||
homeState.stories.forEach {
|
||||
it.isNewWalletAvailable.value = isNewWalletAvailable
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RUSSIA_COUNTRY_CODE = "ru"
|
||||
private const val BELARUS_COUNTRY_CODE = "by"
|
||||
fun initDefaultStories(): List<Stories> = listOf(
|
||||
Stories.TangemIntro,
|
||||
Stories.RevolutionaryWallet,
|
||||
Stories.UltraSecureBackup,
|
||||
Stories.Currencies,
|
||||
Stories.Web3,
|
||||
Stories.WalletForEveryone,
|
||||
)
|
||||
|
||||
fun isNewWalletAvailableInit(): Boolean {
|
||||
val locale = Locale.getDefault().language
|
||||
return !(locale == RUSSIA_COUNTRY_CODE || locale == BELARUS_COUNTRY_CODE)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class Stories(
|
||||
val duration: Int,
|
||||
val isNewWalletAvailable: MutableState<Boolean> = mutableStateOf(HomeState.isNewWalletAvailableInit()),
|
||||
) {
|
||||
object TangemIntro : Stories(duration = 6000)
|
||||
object RevolutionaryWallet : Stories(duration = 6000)
|
||||
object UltraSecureBackup : Stories(duration = 6000)
|
||||
object Currencies : Stories(duration = 6000)
|
||||
object Web3 : Stories(duration = 6000)
|
||||
object WalletForEveryone : Stories(duration = 6000)
|
||||
enum class Stories(val duration: Int = 6000) {
|
||||
TangemIntro,
|
||||
RevolutionaryWallet,
|
||||
UltraSecureBackup,
|
||||
Currencies,
|
||||
Web3,
|
||||
WalletForEveryone,
|
||||
}
|
||||
|
|
@ -13,16 +13,15 @@ import com.tangem.domain.balancehiding.BalanceHidingSettings
|
|||
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.balancehiding.ListenToFlipsUseCase
|
||||
import com.tangem.domain.balancehiding.UpdateBalanceHidingSettingsUseCase
|
||||
import com.tangem.domain.feedback.FeedbackManagerFeatureToggles
|
||||
import com.tangem.domain.settings.DeleteDeprecatedLogsUseCase
|
||||
import com.tangem.domain.settings.IncrementAppLaunchCounterUseCase
|
||||
import com.tangem.domain.settings.usercountry.FetchUserCountryUseCase
|
||||
import com.tangem.domain.staking.FetchStakingTokensUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.features.staking.api.featuretoggles.StakingFeatureToggles
|
||||
import com.tangem.tap.common.extensions.setContext
|
||||
import com.tangem.tap.features.home.featuretoggles.HomeFeatureToggles
|
||||
import com.tangem.tap.features.main.model.MainScreenState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
|
@ -41,12 +40,12 @@ internal class MainViewModel @Inject constructor(
|
|||
private val incrementAppLaunchCounterUseCase: IncrementAppLaunchCounterUseCase,
|
||||
private val blockchainSDKFactory: BlockchainSDKFactory,
|
||||
private val userWalletsListManager: UserWalletsListManager,
|
||||
private val walletManagersFacade: WalletManagersFacade,
|
||||
private val feedbackManagerFeatureToggles: FeedbackManagerFeatureToggles,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
stakingFeatureToggles: StakingFeatureToggles,
|
||||
private val fetchStakingTokensUseCase: FetchStakingTokensUseCase,
|
||||
private val apiConfigsManager: ApiConfigsManager,
|
||||
homeFeatureToggles: HomeFeatureToggles,
|
||||
private val fetchUserCountryUseCase: FetchUserCountryUseCase,
|
||||
getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
|
||||
) : ViewModel(), MainIntents {
|
||||
|
||||
|
|
@ -67,6 +66,14 @@ internal class MainViewModel @Inject constructor(
|
|||
|
||||
viewModelScope.launch(dispatchers.main) { incrementAppLaunchCounterUseCase() }
|
||||
|
||||
if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
|
||||
viewModelScope.launch {
|
||||
fetchUserCountryUseCase().onLeft {
|
||||
Timber.e("Unable to fetch the user country code $it")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateAppCurrencies()
|
||||
observeFlips()
|
||||
displayBalancesHidingStatusToast()
|
||||
|
|
@ -97,19 +104,6 @@ internal class MainViewModel @Inject constructor(
|
|||
.onEach { userWallet ->
|
||||
Analytics.setContext(userWallet.scanResponse)
|
||||
Analytics.send(Basic.WalletOpened())
|
||||
|
||||
if (!feedbackManagerFeatureToggles.isLocalLogsEnabled) {
|
||||
store.state.globalState.feedbackManager?.infoHolder?.let { infoHolder ->
|
||||
infoHolder.setCardInfo(userWallet.scanResponse)
|
||||
|
||||
walletManagersFacade
|
||||
.getAll(userWallet.walletId)
|
||||
.distinctUntilChanged()
|
||||
.onEach(infoHolder::setWalletsInfo)
|
||||
.catch { Timber.e(it) }
|
||||
.launchIn(viewModelScope)
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(dispatchers.io)
|
||||
.launchIn(viewModelScope)
|
||||
|
|
|
|||
|
|
@ -1,22 +1,30 @@
|
|||
package com.tangem.tap.features.onboarding
|
||||
|
||||
import com.tangem.blockchain.common.WalletManager
|
||||
import com.tangem.common.doOnFailure
|
||||
import com.tangem.common.doOnSuccess
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.common.util.twinsIsTwinned
|
||||
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.settings.usercountry.models.UserCountry
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.common.redux.global.GlobalState
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.saveWallet.redux.SaveWalletAction
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
|
|
@ -75,6 +83,56 @@ object OnboardingHelper {
|
|||
}
|
||||
}
|
||||
|
||||
fun saveWallet(
|
||||
scanResponse: ScanResponse,
|
||||
accessCode: String? = null,
|
||||
backupCardsIds: List<String>? = null,
|
||||
hasBackupError: Boolean = false,
|
||||
) {
|
||||
Analytics.setContext(scanResponse)
|
||||
scope.launch {
|
||||
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
|
||||
when {
|
||||
// When should save user wallets, then save card without navigate to save wallet screen
|
||||
store.inject(DaggerGraphState::walletsRepository).shouldSaveUserWalletsSync() -> {
|
||||
store.dispatchWithMain(
|
||||
SaveWalletAction.ProvideBackupInfo(
|
||||
scanResponse = scanResponse,
|
||||
accessCode = accessCode,
|
||||
backupCardsIds = backupCardsIds?.toSet(),
|
||||
),
|
||||
)
|
||||
|
||||
store.dispatchWithMain(
|
||||
SaveWalletAction.SaveWalletAfterBackup(
|
||||
hasBackupError = hasBackupError,
|
||||
shouldNavigateToWallet = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
|
||||
// then open save wallet screen
|
||||
tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen() -> {
|
||||
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
|
||||
|
||||
delay(timeMillis = 1_200)
|
||||
|
||||
store.dispatchOnMain(
|
||||
SaveWalletAction.ProvideBackupInfo(
|
||||
scanResponse = scanResponse,
|
||||
accessCode = accessCode,
|
||||
backupCardsIds = backupCardsIds?.toSet(),
|
||||
),
|
||||
)
|
||||
}
|
||||
// If device has no biometry and save wallet screen has been shown, then go through old scenario
|
||||
else -> {
|
||||
proceedWithScanResponse(scanResponse, backupCardsIds, hasBackupError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun trySaveWalletAndNavigateToWalletScreen(
|
||||
scanResponse: ScanResponse,
|
||||
accessCode: String? = null,
|
||||
|
|
@ -96,7 +154,12 @@ object OnboardingHelper {
|
|||
),
|
||||
)
|
||||
|
||||
store.dispatchWithMain(SaveWalletAction.SaveWalletAfterBackup(hasBackupError))
|
||||
store.dispatchWithMain(
|
||||
SaveWalletAction.SaveWalletAfterBackup(
|
||||
hasBackupError = hasBackupError,
|
||||
shouldNavigateToWallet = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
// When should not save user wallets but device has biometry and save wallet screen has not been shown,
|
||||
// then open save wallet screen
|
||||
|
|
@ -138,6 +201,39 @@ object OnboardingHelper {
|
|||
}
|
||||
}
|
||||
|
||||
fun handleTopUpAction(walletManager: WalletManager, scanResponse: ScanResponse, globalState: GlobalState) {
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
val cryptoCurrency = CryptoCurrencyFactory().createCoin(
|
||||
blockchain = blockchain,
|
||||
extraDerivationPath = null,
|
||||
scanResponse = scanResponse,
|
||||
) ?: return
|
||||
|
||||
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
|
||||
|
||||
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
|
||||
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
|
||||
|
||||
scope.launch {
|
||||
val homeFeatureToggles = store.inject(DaggerGraphState::homeFeatureToggles)
|
||||
|
||||
val isRussia = if (homeFeatureToggles.isMigrateUserCountryCodeEnabled) {
|
||||
val getUserCountryCodeUseCase = store.inject(DaggerGraphState::getUserCountryUseCase)
|
||||
|
||||
getUserCountryCodeUseCase().isRight { it is UserCountry.Russia }
|
||||
} else {
|
||||
globalState.userCountryCode == RUSSIA_COUNTRY_CODE
|
||||
}
|
||||
|
||||
if (isRussia) {
|
||||
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
|
||||
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
|
||||
} else {
|
||||
store.dispatchOpenUrl(topUpUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithScanResponse(
|
||||
scanResponse: ScanResponse,
|
||||
backupCardsIds: List<String>?,
|
||||
|
|
@ -160,6 +256,7 @@ object OnboardingHelper {
|
|||
}
|
||||
.doOnSuccess {
|
||||
mainScope.launch { store.onUserWalletSelected(userWallet) }
|
||||
store.dispatch(OnboardingWalletAction.WalletSaved(userWallet.walletId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,6 @@ import com.tangem.operations.attestation.CardVerifyAndGetInfo
|
|||
import com.tangem.operations.attestation.OnlineCardVerifier
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.extensions.isPositive
|
||||
import com.tangem.tap.common.extensions.safeUpdate
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.extensions.getOrLoadCardArtworkUrl
|
||||
|
|
@ -20,6 +19,7 @@ import com.tangem.tap.domain.model.hasPendingTransactions
|
|||
import com.tangem.tap.features.demo.isDemoCard
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.extensions.isPositive
|
||||
import timber.log.Timber
|
||||
import java.math.BigDecimal
|
||||
|
||||
|
|
|
|||
|
|
@ -7,12 +7,15 @@ import androidx.core.view.MenuProvider
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -28,13 +31,15 @@ class OnboardingMenuProvider(
|
|||
override fun onMenuItemSelected(menuItem: MenuItem): Boolean = when (menuItem.itemId) {
|
||||
R.id.menu_item_chat_support -> {
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
|
||||
// changed on email support [REDACTED_TASK_KEY]
|
||||
store.dispatch(
|
||||
GlobalAction.SendEmail(
|
||||
feedbackData = SupportInfo(),
|
||||
scanResponse = scanResponseProvider(),
|
||||
),
|
||||
)
|
||||
|
||||
val cardInfo = store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponseProvider()).getOrNull()
|
||||
?: error("CardInfo must be not null")
|
||||
|
||||
scope.launch {
|
||||
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
|
||||
.invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo))
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import com.tangem.core.navigation.ShareElement
|
|||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.extensions.getDrawableCompat
|
||||
import com.tangem.tap.common.extensions.stripZeroPlainString
|
||||
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
|
||||
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
|
||||
import com.tangem.tap.features.addBackPressHandler
|
||||
|
|
@ -24,6 +23,7 @@ import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteActi
|
|||
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteState
|
||||
import com.tangem.tap.features.onboarding.products.note.redux.OnboardingNoteStep
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.extensions.stripZeroPlainString
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -4,12 +4,8 @@ import com.tangem.common.CompletionResult
|
|||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.routing.AppRouter
|
||||
import com.tangem.core.analytics.Analytics
|
||||
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.extensions.makePrimaryWalletManager
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
import com.tangem.tap.common.extensions.*
|
||||
|
|
@ -20,7 +16,6 @@ import com.tangem.tap.common.redux.global.GlobalAction
|
|||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.model.Currency
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.onboarding.OnboardingDialog
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.mainScope
|
||||
|
|
@ -178,23 +173,11 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
|
|||
return
|
||||
}
|
||||
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
val cryptoCurrency = CryptoCurrencyFactory().createCoin(
|
||||
blockchain,
|
||||
null,
|
||||
scanResponse.derivationStyleProvider,
|
||||
) ?: return
|
||||
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
|
||||
|
||||
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
|
||||
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
|
||||
|
||||
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
|
||||
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
|
||||
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
|
||||
} else {
|
||||
store.dispatchOpenUrl(topUpUrl)
|
||||
}
|
||||
OnboardingHelper.handleTopUpAction(
|
||||
walletManager = walletManager,
|
||||
scanResponse = scanResponse,
|
||||
globalState = globalState,
|
||||
)
|
||||
}
|
||||
is OnboardingNoteAction.Done -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Stop)
|
||||
|
|
|
|||
|
|
@ -6,16 +6,12 @@ import com.tangem.common.extensions.guard
|
|||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.utils.popTo
|
||||
import com.tangem.core.analytics.Analytics
|
||||
|
||||
import com.tangem.data.common.currency.CryptoCurrencyFactory
|
||||
import com.tangem.domain.common.extensions.makePrimaryWalletManager
|
||||
import com.tangem.domain.common.extensions.withMainContext
|
||||
import com.tangem.domain.common.util.derivationStyleProvider
|
||||
import com.tangem.domain.common.util.twinsIsTwinned
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.legacy.asLockable
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.entities.ProgressState
|
||||
import com.tangem.tap.common.extensions.*
|
||||
|
|
@ -26,7 +22,6 @@ import com.tangem.tap.common.redux.global.GlobalAction
|
|||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.model.Currency
|
||||
import com.tangem.tap.domain.twins.TwinCardsManager
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.onboarding.OnboardingDialog
|
||||
import com.tangem.tap.features.onboarding.OnboardingHelper
|
||||
import com.tangem.tap.mainScope
|
||||
|
|
@ -301,24 +296,11 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
|
|||
return
|
||||
}
|
||||
|
||||
val scanResponse = onboardingManager?.scanResponse ?: return
|
||||
val blockchain = walletManager.wallet.blockchain
|
||||
val cryptoCurrency = CryptoCurrencyFactory().createCoin(
|
||||
blockchain,
|
||||
null,
|
||||
scanResponse.derivationStyleProvider,
|
||||
) ?: return
|
||||
val topUpUrl = walletManager.getTopUpUrl(cryptoCurrency) ?: return
|
||||
|
||||
val currencyType = AnalyticsParam.CurrencyType.Blockchain(blockchain)
|
||||
Analytics.send(Onboarding.Topup.ButtonBuyCrypto(currencyType))
|
||||
|
||||
if (globalState.userCountryCode == RUSSIA_COUNTRY_CODE) {
|
||||
val dialogData = AppDialog.RussianCardholdersWarningDialog.Data(topUpUrl)
|
||||
store.dispatchDialogShow(AppDialog.RussianCardholdersWarningDialog(dialogData))
|
||||
} else {
|
||||
store.dispatchOpenUrl(topUpUrl)
|
||||
}
|
||||
OnboardingHelper.handleTopUpAction(
|
||||
walletManager = walletManager,
|
||||
scanResponse = onboardingManager?.scanResponse ?: return,
|
||||
globalState = globalState,
|
||||
)
|
||||
}
|
||||
TwinCardsAction.Done -> {
|
||||
val scanResponse = getScanResponse()
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ import com.tangem.domain.models.scan.ScanResponse
|
|||
import com.tangem.domain.wallets.models.Artwork
|
||||
import com.tangem.sdk.ui.widget.leapfrogWidget.LeapfrogWidget
|
||||
import com.tangem.tap.common.analytics.events.Onboarding
|
||||
import com.tangem.tap.common.extensions.*
|
||||
import com.tangem.tap.common.extensions.beginDelayedTransition
|
||||
import com.tangem.tap.common.extensions.getDrawableCompat
|
||||
import com.tangem.tap.common.extensions.hide
|
||||
import com.tangem.tap.common.extensions.show
|
||||
import com.tangem.tap.common.toggleWidget.RefreshBalanceWidget
|
||||
import com.tangem.tap.common.transitions.InternalNoteLayoutTransition
|
||||
import com.tangem.tap.domain.twins.TwinsCardWidget
|
||||
|
|
@ -35,6 +38,7 @@ import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
|
|||
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsStep
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.utils.extensions.stripZeroPlainString
|
||||
import com.tangem.wallet.R
|
||||
import com.tangem.wallet.databinding.LayoutOnboardingContainerTopBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package com.tangem.tap.features.onboarding.products.wallet.redux
|
|||
|
||||
import android.net.Uri
|
||||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
|
||||
|
|
@ -29,6 +30,8 @@ sealed class OnboardingWalletAction : Action {
|
|||
class SetThirdCardArtworkUrl(val artworkUri: Uri?) : OnboardingWalletAction()
|
||||
|
||||
data object OnBackPressed : OnboardingWalletAction()
|
||||
|
||||
data class WalletSaved(val userWalletId: UserWalletId) : OnboardingWalletAction()
|
||||
}
|
||||
|
||||
sealed class OnboardingWallet2Action : OnboardingWalletAction() {
|
||||
|
|
@ -91,6 +94,8 @@ sealed class BackupAction : Action {
|
|||
|
||||
data class FinishBackup(val withAnalytics: Boolean = true) : BackupAction()
|
||||
|
||||
data class BackupFinished(val userWalletId: UserWalletId?) : BackupAction()
|
||||
|
||||
data object DiscardBackup : BackupAction()
|
||||
data object DiscardSavedBackup : BackupAction()
|
||||
data object ResumeFoundUnfinishedBackup : BackupAction()
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
|
|||
import com.tangem.common.CompletionResult
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.common.core.TangemSdkError
|
||||
import com.tangem.common.extensions.guard
|
||||
import com.tangem.common.extensions.ifNotNull
|
||||
import com.tangem.common.extensions.toHexString
|
||||
import com.tangem.common.routing.AppRoute
|
||||
|
|
@ -20,8 +19,10 @@ import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_IDS
|
|||
import com.tangem.domain.models.scan.CardDTO.Companion.RING_BATCH_PREFIX
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.wallets.builder.UserWalletBuilder
|
||||
import com.tangem.domain.wallets.legacy.UserWalletsListManager
|
||||
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
|
||||
import com.tangem.domain.wallets.models.Artwork
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.feature.onboarding.data.model.CreateWalletResponse
|
||||
import com.tangem.feature.onboarding.presentation.wallet2.analytics.SeedPhraseSource
|
||||
import com.tangem.feature.wallet.presentation.wallet.domain.BackupValidator
|
||||
|
|
@ -167,16 +168,7 @@ private fun handleWalletAction(action: Action) {
|
|||
}
|
||||
is OnboardingWalletAction.FinishOnboarding -> {
|
||||
store.dispatch(GlobalAction.Onboarding.Stop)
|
||||
|
||||
if (scanResponse == null) {
|
||||
action.scope.launch {
|
||||
readCard { newScanResponse ->
|
||||
handleFinishOnboardind(newScanResponse)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleFinishOnboardind(scanResponse)
|
||||
}
|
||||
navigateToWalletScreen()
|
||||
}
|
||||
is OnboardingWalletAction.ResumeBackup -> {
|
||||
val newAction = when (val backupState = backupService.currentState) {
|
||||
|
|
@ -203,7 +195,7 @@ private fun handleWalletAction(action: Action) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun handleFinishOnboardind(scanResponse: ScanResponse) {
|
||||
private fun handleFinishBackup(scanResponse: ScanResponse) {
|
||||
val backupState = store.state.onboardingWalletState.backupState
|
||||
val updatedScanResponse = updateScanResponseAfterBackup(scanResponse, backupState)
|
||||
|
||||
|
|
@ -214,7 +206,7 @@ private fun handleFinishOnboardind(scanResponse: ScanResponse) {
|
|||
}
|
||||
}
|
||||
|
||||
OnboardingHelper.trySaveWalletAndNavigateToWalletScreen(
|
||||
OnboardingHelper.saveWallet(
|
||||
scanResponse = updatedScanResponse,
|
||||
accessCode = backupState.accessCode,
|
||||
backupCardsIds = backupState.backupCardIds,
|
||||
|
|
@ -222,7 +214,18 @@ private fun handleFinishOnboardind(scanResponse: ScanResponse) {
|
|||
)
|
||||
}
|
||||
|
||||
private suspend fun readCard(onSuccess: (ScanResponse) -> Unit) {
|
||||
private fun navigateToWalletScreen() {
|
||||
mainScope.launch {
|
||||
val settingsRepository = store.inject(DaggerGraphState::settingsRepository)
|
||||
store.dispatchNavigationAction { replaceAll(AppRoute.Wallet) }
|
||||
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
|
||||
delay(timeMillis = 1_800)
|
||||
store.dispatchNavigationAction { push(AppRoute.SaveWallet) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun readCard(onSuccess: suspend (ScanResponse) -> Unit) {
|
||||
val shouldSaveAccessCodes = store.inject(DaggerGraphState::settingsRepository).shouldSaveAccessCodes()
|
||||
|
||||
store.inject(DaggerGraphState::cardSdkConfigRepository).setAccessCodeRequestPolicy(
|
||||
|
|
@ -417,7 +420,7 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
val onboardingWalletState = appState()?.onboardingWalletState ?: return
|
||||
|
||||
val backupState = onboardingWalletState.backupState
|
||||
val scanResponse = globalState.onboardingState.onboardingManager?.scanResponse
|
||||
var scanResponse = globalState.onboardingState.onboardingManager?.scanResponse
|
||||
val card = scanResponse?.card
|
||||
|
||||
when (action) {
|
||||
|
|
@ -599,6 +602,15 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
cardIds = gatherCardIds(backupState, card),
|
||||
)
|
||||
}
|
||||
if (scanResponse == null) {
|
||||
scope.launch {
|
||||
readCard { newScanResponse ->
|
||||
handleFinishBackup(newScanResponse)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
handleFinishBackup(scanResponse)
|
||||
}
|
||||
}
|
||||
is BackupAction.FinishBackup -> {
|
||||
scope.launch {
|
||||
|
|
@ -606,28 +618,32 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
Analytics.send(Onboarding.Backup.Finished(backupState.backupCardsNumber))
|
||||
}
|
||||
|
||||
var userWallet: UserWallet? = null
|
||||
if (scanResponse != null) {
|
||||
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
|
||||
val userWallet = UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
|
||||
.backupCardsIds(backupState.backupCardIds.toSet())
|
||||
.build()
|
||||
.guard {
|
||||
Timber.e("User wallet not created")
|
||||
return@launch
|
||||
}
|
||||
userWallet = createUserWallet(
|
||||
scanResponse = requireNotNull(value = scanResponse, lazyMessage = { "ScanResponse is null" }),
|
||||
backupState = backupState,
|
||||
)
|
||||
|
||||
val userWalletsListManager = store.inject(DaggerGraphState::generalUserWalletsListManager)
|
||||
userWalletsListManager.update(
|
||||
userWalletId = userWallet.walletId,
|
||||
update = { wallet ->
|
||||
wallet.copy(
|
||||
scanResponse = updateScanResponseAfterBackup(
|
||||
scanResponse = wallet.scanResponse,
|
||||
backupState = backupState,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
when (backupState.startedSource) {
|
||||
BackupStartedSource.Onboarding -> saveWallet(
|
||||
userWalletsListManager = userWalletsListManager,
|
||||
userWallet = userWallet,
|
||||
scanResponse = scanResponse,
|
||||
backupState = backupState,
|
||||
)
|
||||
BackupStartedSource.CreateBackup -> updateWallet(
|
||||
userWalletsListManager = userWalletsListManager,
|
||||
userWallet = userWallet,
|
||||
backupState = backupState,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
readCard { newScanResponse ->
|
||||
scanResponse = newScanResponse
|
||||
userWallet = createUserWallet(newScanResponse, backupState)
|
||||
}
|
||||
}
|
||||
|
||||
val notActivatedCardIds = gatherCardIds(backupState, card).mapNotNull {
|
||||
|
|
@ -639,11 +655,18 @@ private fun handleBackupAction(appState: () -> AppState?, action: BackupAction)
|
|||
}
|
||||
|
||||
// All cardIds may already be activated if the backup was skipped before.
|
||||
if (notActivatedCardIds.isEmpty()) return@launch
|
||||
if (notActivatedCardIds.isEmpty()) {
|
||||
delay(1000)
|
||||
store.dispatch(BackupAction.BackupFinished(userWallet?.walletId))
|
||||
return@launch
|
||||
}
|
||||
|
||||
Analytics.send(Onboarding.Finished())
|
||||
|
||||
store.state.globalState.onboardingState.onboardingManager?.finishActivation(notActivatedCardIds)
|
||||
handleFinishBackup(requireNotNull(scanResponse))
|
||||
delay(1000)
|
||||
store.dispatch(BackupAction.BackupFinished(userWalletId = userWallet?.walletId))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -659,6 +682,54 @@ private fun Card?.isRing(): Boolean {
|
|||
return this?.let { RING_BATCH_IDS.contains(batchId) || batchId.startsWith(RING_BATCH_PREFIX) } ?: false
|
||||
}
|
||||
|
||||
private suspend fun saveWallet(
|
||||
userWalletsListManager: UserWalletsListManager,
|
||||
userWallet: UserWallet,
|
||||
scanResponse: ScanResponse?,
|
||||
backupState: BackupState,
|
||||
) {
|
||||
userWalletsListManager.save(
|
||||
userWallet = userWallet.copy(
|
||||
scanResponse = updateScanResponseAfterBackup(
|
||||
scanResponse = requireNotNull(
|
||||
value = scanResponse,
|
||||
lazyMessage = { "ScanResponse is null" },
|
||||
),
|
||||
backupState = backupState,
|
||||
),
|
||||
),
|
||||
canOverride = true,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun updateWallet(
|
||||
userWalletsListManager: UserWalletsListManager,
|
||||
userWallet: UserWallet,
|
||||
backupState: BackupState,
|
||||
) {
|
||||
userWalletsListManager.update(
|
||||
userWalletId = userWallet.walletId,
|
||||
update = { wallet ->
|
||||
wallet.copy(
|
||||
scanResponse = updateScanResponseAfterBackup(
|
||||
scanResponse = wallet.scanResponse,
|
||||
backupState = backupState,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun createUserWallet(scanResponse: ScanResponse, backupState: BackupState): UserWallet {
|
||||
val walletNameGenerateUseCase = store.inject(DaggerGraphState::generateWalletNameUseCase)
|
||||
return requireNotNull(
|
||||
value = UserWalletBuilder(scanResponse, walletNameGenerateUseCase)
|
||||
.backupCardsIds(backupState.backupCardIds.toSet())
|
||||
.build(),
|
||||
lazyMessage = { "User wallet not created" },
|
||||
)
|
||||
}
|
||||
|
||||
fun updateArtworks(addedBackupCardsCount: Int, card: Card) {
|
||||
mainScope.launch {
|
||||
withIOContext {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package com.tangem.tap.features.onboarding.products.wallet.redux
|
||||
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.redux.OnboardingManageTokensAction
|
||||
import com.tangem.tap.backupService
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
|
|
@ -9,12 +11,24 @@ object OnboardingWalletReducer {
|
|||
fun reduce(action: Action, state: AppState): OnboardingWalletState = internalReduce(action, state)
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private fun internalReduce(action: Action, appState: AppState): OnboardingWalletState {
|
||||
val state = appState.onboardingWalletState
|
||||
val backupState = state.backupState
|
||||
|
||||
return when (action) {
|
||||
is GlobalAction.Onboarding -> ReducerForGlobalAction.reduce(action, state)
|
||||
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action, state.backupState))
|
||||
is BackupAction.BackupFinished -> {
|
||||
state.copy(
|
||||
step = if (action.userWalletId != null && backupState.startedSource == BackupStartedSource.Onboarding) {
|
||||
OnboardingWalletStep.ManageTokens
|
||||
} else {
|
||||
OnboardingWalletStep.Done
|
||||
},
|
||||
userWalletId = action.userWalletId,
|
||||
)
|
||||
}
|
||||
is BackupAction -> state.copy(backupState = BackupReducer.reduce(action = action, state = state.backupState))
|
||||
is OnboardingWallet2Action -> OnboardingWallet2Reducer.reduce(action, state)
|
||||
is OnboardingWalletAction.GetToCreateWalletStep -> state.copy(
|
||||
step = OnboardingWalletStep.CreateWallet,
|
||||
|
|
@ -35,6 +49,14 @@ private fun internalReduce(action: Action, appState: AppState): OnboardingWallet
|
|||
walletImages = state.walletImages.copy(thirdCardImage = action.artworkUri),
|
||||
)
|
||||
}
|
||||
is OnboardingManageTokensAction.CurrenciesSaved -> state.copy(step = OnboardingWalletStep.Done)
|
||||
is OnboardingWalletAction.WalletSaved -> state.copy(
|
||||
step = when (backupState.startedSource) {
|
||||
BackupStartedSource.Onboarding -> OnboardingWalletStep.ManageTokens
|
||||
BackupStartedSource.CreateBackup -> OnboardingWalletStep.Done
|
||||
},
|
||||
userWalletId = action.userWalletId,
|
||||
)
|
||||
is OnboardingWalletAction.Done -> state.copy(step = OnboardingWalletStep.Done)
|
||||
else -> state
|
||||
}
|
||||
|
|
@ -51,6 +73,7 @@ private object ReducerForGlobalAction {
|
|||
backupState = state.backupState.copy(
|
||||
maxBackupCards = MAX_BACKUP_CARDS,
|
||||
canSkipBackup = action.canSkipBackup,
|
||||
startedSource = action.source,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -87,6 +110,7 @@ private object BackupReducer {
|
|||
backupStep = BackupStep.InitBackup,
|
||||
maxBackupCards = 2,
|
||||
canSkipBackup = state.canSkipBackup,
|
||||
startedSource = state.startedSource,
|
||||
)
|
||||
BackupAction.StartAddingPrimaryCard -> {
|
||||
state.copy(
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.graphics.Bitmap
|
|||
import android.net.Uri
|
||||
import com.tangem.common.card.Card
|
||||
import com.tangem.domain.redux.StateDialog
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import org.rekotlin.StateType
|
||||
|
||||
/**
|
||||
|
|
@ -15,18 +16,20 @@ data class OnboardingWalletState(
|
|||
val backupState: BackupState = BackupState(),
|
||||
val walletImages: WalletImages = WalletImages(),
|
||||
val showConfetti: Boolean = false,
|
||||
val isRingOnboarding: Boolean = false,
|
||||
val userWalletId: UserWalletId? = null,
|
||||
) : StateType {
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
fun getMaxProgress(): Int {
|
||||
val baseProgress = 6
|
||||
val baseProgress = 7
|
||||
return getWallet2Progress() + baseProgress
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod", "MagicNumber")
|
||||
fun getProgressStep(): Int {
|
||||
val progressByStep = when (step) {
|
||||
OnboardingWalletStep.CreateWallet -> 1
|
||||
OnboardingWalletStep.CreateWallet, OnboardingWalletStep.None -> 1
|
||||
OnboardingWalletStep.Backup -> {
|
||||
when (backupState.backupStep) {
|
||||
null -> 2
|
||||
|
|
@ -37,11 +40,11 @@ data class OnboardingWalletState(
|
|||
BackupStep.ReenterAccessCode -> 4
|
||||
BackupStep.SetAccessCode -> 4
|
||||
BackupStep.WritePrimaryCard, is BackupStep.WriteBackupCard -> 5
|
||||
BackupStep.Finished -> getMaxProgress()
|
||||
BackupStep.Finished -> 6
|
||||
}
|
||||
}
|
||||
OnboardingWalletStep.ManageTokens -> 6
|
||||
OnboardingWalletStep.Done -> getMaxProgress()
|
||||
else -> 1
|
||||
}
|
||||
|
||||
return getWallet2Progress() + progressByStep
|
||||
|
|
@ -61,7 +64,7 @@ data class OnboardingWallet2State(
|
|||
)
|
||||
|
||||
enum class OnboardingWalletStep {
|
||||
None, CreateWallet, Backup, Done
|
||||
None, CreateWallet, Backup, ManageTokens, Done
|
||||
}
|
||||
|
||||
data class BackupState(
|
||||
|
|
@ -81,9 +84,14 @@ data class BackupState(
|
|||
val isInterruptedBackup: Boolean = false,
|
||||
val showBtnLoading: Boolean = false,
|
||||
val hasBackupError: Boolean = false,
|
||||
val startedSource: BackupStartedSource = BackupStartedSource.Onboarding,
|
||||
val hasRing: Boolean = false,
|
||||
)
|
||||
|
||||
enum class BackupStartedSource {
|
||||
Onboarding, CreateBackup
|
||||
}
|
||||
|
||||
enum class AccessCodeError {
|
||||
CodeTooShort, CodesDoNotMatch
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package com.tangem.tap.features.onboarding.products.wallet.ui
|
|||
import android.content.res.ColorStateList
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
|
|
@ -10,12 +12,16 @@ import android.view.WindowManager
|
|||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.view.MenuProvider
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.transition.TransitionManager
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import coil.load
|
||||
import com.arkivanov.decompose.defaultComponentContext
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import com.tangem.common.CardIdFormatter
|
||||
|
|
@ -25,21 +31,29 @@ import com.tangem.common.routing.AppRoute
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.core.decompose.context.AppComponentContext
|
||||
import com.tangem.core.decompose.context.childByContext
|
||||
import com.tangem.core.decompose.di.RootAppComponentContext
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.extensions.setStatusBarColor
|
||||
import com.tangem.core.ui.message.EventMessageEffect
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.windowsize.rememberWindowSize
|
||||
import com.tangem.datasource.utils.isNullOrEmpty
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.models.scan.isRing
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
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.features.managetokens.component.OnboardingManageTokensComponent
|
||||
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.*
|
||||
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
|
||||
|
|
@ -47,6 +61,8 @@ import com.tangem.tap.features.onboarding.OnboardingMenuProvider
|
|||
import com.tangem.tap.features.onboarding.products.wallet.redux.*
|
||||
import com.tangem.tap.features.onboarding.products.wallet.ui.dialogs.AccessCodeDialog
|
||||
import com.tangem.tap.mainScope
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.utils.Provider
|
||||
import com.tangem.wallet.R
|
||||
|
|
@ -56,6 +72,7 @@ import com.tangem.wallet.databinding.ViewOnboardingProgressBinding
|
|||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.rekotlin.StoreSubscriber
|
||||
import javax.inject.Inject
|
||||
|
||||
@Suppress("LargeClass", "MagicNumber")
|
||||
@AndroidEntryPoint
|
||||
|
|
@ -64,10 +81,24 @@ class OnboardingWalletFragment :
|
|||
StoreSubscriber<OnboardingWalletState>,
|
||||
FragmentOnBackPressedHandler {
|
||||
|
||||
@Inject
|
||||
internal lateinit var uiDependencies: UiDependencies
|
||||
|
||||
@Inject
|
||||
internal lateinit var onboardingManageTokensComponentFactory: OnboardingManageTokensComponent.Factory
|
||||
|
||||
@Inject
|
||||
@RootAppComponentContext
|
||||
internal lateinit var rootComponentContext: AppComponentContext
|
||||
|
||||
private var onboardingManageTokensComponent: OnboardingManageTokensComponent? = null
|
||||
private lateinit var onboardingComponentContext: AppComponentContext
|
||||
|
||||
internal val binding: FragmentOnboardingWalletBinding by viewBinding(FragmentOnboardingWalletBinding::bind)
|
||||
internal val pbBinding: ViewOnboardingProgressBinding by viewBinding(ViewOnboardingProgressBinding::bind)
|
||||
|
||||
internal val bindingSeedPhrase: LayoutOnboardingSeedPhraseBinding by lazy { binding.onboardingSeedPhraseContainer }
|
||||
private val bindingManageTokens by lazy { binding.onboardingManageTokensContainer }
|
||||
|
||||
private val canSkipBackup by lazy { arguments?.getBoolean(AppRoute.OnboardingWallet.CAN_SKIP_BACKUP_KEY) ?: true }
|
||||
|
||||
|
|
@ -89,6 +120,10 @@ class OnboardingWalletFragment :
|
|||
seedPhraseRouter = newSeedPhraseRouter
|
||||
seedPhraseViewModel.setRouter(newSeedPhraseRouter)
|
||||
seedPhraseViewModel.setMediator(makeSeedPhraseMediator())
|
||||
|
||||
onboardingComponentContext = rootComponentContext.childByContext(
|
||||
componentContext = defaultComponentContext(onBackPressedDispatcher = null),
|
||||
)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
|
@ -173,27 +208,29 @@ class OnboardingWalletFragment :
|
|||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
onboardingManageTokensComponent = null
|
||||
store.unsubscribe(this)
|
||||
}
|
||||
|
||||
override fun newState(state: OnboardingWalletState) {
|
||||
if (activity == null || view == null) return
|
||||
Handler(Looper.getMainLooper()).post {
|
||||
if (activity == null || view == null) return@post
|
||||
animator.updateBackupState(state.backupState)
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
|
||||
animator.updateBackupState(state.backupState)
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
pbBinding.pbState.max = state.getMaxProgress()
|
||||
pbBinding.pbState.progress = state.getProgressStep()
|
||||
|
||||
pbBinding.pbState.max = state.getMaxProgress()
|
||||
pbBinding.pbState.progress = state.getProgressStep()
|
||||
|
||||
when {
|
||||
state.wallet2State != null -> {
|
||||
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
|
||||
seedPhraseViewModel.setCardArtworkUri(cardArtworkUri = state.walletImages.primaryCardImage.toString())
|
||||
updateWalletImagesState(state.walletImages)
|
||||
}
|
||||
else -> {
|
||||
updateWalletImagesState(state.walletImages)
|
||||
handleOnboardingStep(state)
|
||||
when {
|
||||
state.wallet2State != null -> {
|
||||
seedPhraseStateHandler.newState(this, state, seedPhraseViewModel)
|
||||
seedPhraseViewModel.setCardArtworkUri(cardArtworkUri = state.walletImages.primaryCardImage.toString())
|
||||
updateWalletImagesState(state.walletImages)
|
||||
}
|
||||
else -> {
|
||||
updateWalletImagesState(state.walletImages)
|
||||
handleOnboardingStep(state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -221,14 +258,54 @@ class OnboardingWalletFragment :
|
|||
internal fun handleOnboardingStep(state: OnboardingWalletState) {
|
||||
when (state.step) {
|
||||
OnboardingWalletStep.CreateWallet -> setupCreateWalletState()
|
||||
OnboardingWalletStep.Backup -> setBackupState(
|
||||
state = state.backupState,
|
||||
)
|
||||
|
||||
OnboardingWalletStep.Backup -> setBackupState(state = state.backupState)
|
||||
OnboardingWalletStep.ManageTokens -> setManageTokensState(requireNotNull(state.userWalletId))
|
||||
OnboardingWalletStep.Done -> showSuccess()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeOnboardingManageTokensComponent(userWalletId: UserWalletId) {
|
||||
onboardingManageTokensComponent = onboardingManageTokensComponentFactory.create(
|
||||
context = onboardingComponentContext,
|
||||
params = OnboardingManageTokensComponent.Params(userWalletId = userWalletId),
|
||||
)
|
||||
}
|
||||
|
||||
private fun setManageTokensState(userWalletId: UserWalletId) {
|
||||
with(binding) {
|
||||
tvHeader.show()
|
||||
tvBody.show()
|
||||
viewPagerBackupInfo.hide()
|
||||
tabLayoutBackupInfo.hide()
|
||||
onboardingWalletContainer.show()
|
||||
layoutButtonsCommon.btnWalletAlternativeAction.hide()
|
||||
onboardingWalletContainer.hide()
|
||||
toolbar.title = getText(R.string.onboarding_add_tokens)
|
||||
}
|
||||
pbBinding.pbState.show()
|
||||
bindingSeedPhrase.onboardingSeedPhraseContainer.hide()
|
||||
if (onboardingManageTokensComponent == null) {
|
||||
initializeOnboardingManageTokensComponent(userWalletId)
|
||||
}
|
||||
with(bindingManageTokens) {
|
||||
onboardingManageTokensContainer.hide()
|
||||
onboardingManageTokensContainer.show()
|
||||
onboardingManageTokensContainer.setContent {
|
||||
TangemTheme(
|
||||
isDark = isSystemInDarkTheme(),
|
||||
windowSize = rememberWindowSize(activity = requireActivity()),
|
||||
) {
|
||||
onboardingManageTokensComponent?.Content(modifier = Modifier.fillMaxSize())
|
||||
EventMessageEffect(
|
||||
messageHandler = uiDependencies.eventMessageHandler,
|
||||
snackbarHostState = uiDependencies.globalSnackbarHostState,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupCreateWalletState() = with(binding) {
|
||||
layoutButtonsCommon.btnWalletMainAction.setText(R.string.onboarding_create_wallet_button_create_wallet)
|
||||
layoutButtonsCommon.btnWalletMainAction.setIconResource(R.drawable.ic_tangem_24)
|
||||
|
|
@ -258,7 +335,10 @@ class OnboardingWalletFragment :
|
|||
BackupStep.ReenterAccessCode -> showReenterAccessCode(state)
|
||||
is BackupStep.WritePrimaryCard -> showWritePrimaryCard(state)
|
||||
is BackupStep.WriteBackupCard -> showWriteBackupCard(state)
|
||||
BackupStep.Finished -> showSuccess()
|
||||
BackupStep.Finished -> {
|
||||
// don't need to navigate here"
|
||||
// showSuccess()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -479,6 +559,8 @@ class OnboardingWalletFragment :
|
|||
tvBody.show()
|
||||
viewPagerBackupInfo.hide()
|
||||
tabLayoutBackupInfo.hide()
|
||||
onboardingWalletContainer.show()
|
||||
bindingManageTokens.onboardingManageTokensContainer.hide()
|
||||
|
||||
tvBody.text = getText(R.string.onboarding_subtitle_success_tangem_wallet_onboarding)
|
||||
layoutButtonsCommon.btnWalletMainAction.text = getText(R.string.onboarding_button_continue_wallet)
|
||||
|
|
@ -536,14 +618,16 @@ class OnboardingWalletFragment :
|
|||
onBack = ::legacyOnBackHandler,
|
||||
onOpenChat = {
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
|
||||
// changed on email support [REDACTED_TASK_KEY]
|
||||
store.dispatch(
|
||||
GlobalAction.SendEmail(
|
||||
feedbackData = SupportInfo(),
|
||||
scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse
|
||||
?: error("ScanResponse must be not null"),
|
||||
),
|
||||
)
|
||||
|
||||
val scanResponse = requireNotNull(store.state.globalState.onboardingState.onboardingManager?.scanResponse)
|
||||
|
||||
val getCardInfoUseCase = store.inject(DaggerGraphState::getCardInfoUseCase)
|
||||
val cardInfo = requireNotNull(getCardInfoUseCase.invoke(scanResponse).getOrNull())
|
||||
|
||||
scope.launch {
|
||||
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
|
||||
.invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo))
|
||||
}
|
||||
},
|
||||
onOpenUriClick = { uri ->
|
||||
store.dispatchOpenUrl(uri.toString())
|
||||
|
|
|
|||
|
|
@ -6,12 +6,15 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.models.AnalyticsParam
|
||||
import com.tangem.core.analytics.models.Basic
|
||||
import com.tangem.domain.feedback.models.FeedbackEmailType
|
||||
import com.tangem.tap.common.extensions.dispatchDialogHide
|
||||
import com.tangem.tap.common.feedback.SupportInfo
|
||||
import com.tangem.tap.common.redux.global.GlobalAction
|
||||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.features.onboarding.OnboardingDialog
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
object WalletActivationErrorDialog {
|
||||
|
||||
|
|
@ -23,13 +26,17 @@ object WalletActivationErrorDialog {
|
|||
setNegativeButton(R.string.common_support) { _, _ ->
|
||||
// changed on email support [REDACTED_TASK_KEY]
|
||||
Analytics.send(Basic.ButtonSupport(AnalyticsParam.ScreensSources.Intro))
|
||||
store.dispatch(
|
||||
GlobalAction.SendEmail(
|
||||
feedbackData = SupportInfo(),
|
||||
scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse
|
||||
?: error("ScanResponse must be not null"),
|
||||
),
|
||||
)
|
||||
|
||||
val scanResponse = store.state.globalState.onboardingState.onboardingManager?.scanResponse
|
||||
?: error("ScanResponse must be not null")
|
||||
|
||||
val cardInfo = store.inject(DaggerGraphState::getCardInfoUseCase).invoke(scanResponse).getOrNull()
|
||||
?: error("CardInfo must be not null")
|
||||
|
||||
scope.launch {
|
||||
store.inject(DaggerGraphState::sendFeedbackEmailUseCase)
|
||||
.invoke(type = FeedbackEmailType.DirectUserRequest(cardInfo))
|
||||
}
|
||||
}
|
||||
setOnDismissListener { store.dispatchDialogHide() }
|
||||
setCancelable(false)
|
||||
|
|
|
|||
|
|
@ -24,5 +24,8 @@ internal sealed interface SaveWalletAction : Action {
|
|||
data object Cancel : SaveWalletAction
|
||||
}
|
||||
|
||||
data class SaveWalletAfterBackup(val hasBackupError: Boolean) : SaveWalletAction
|
||||
data class SaveWalletAfterBackup(
|
||||
val hasBackupError: Boolean,
|
||||
val shouldNavigateToWallet: Boolean,
|
||||
) : SaveWalletAction
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import com.tangem.tap.common.extensions.dispatchWithMain
|
|||
import com.tangem.tap.common.extensions.inject
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.features.onboarding.products.wallet.redux.OnboardingWalletAction
|
||||
import com.tangem.tap.proxy.redux.DaggerGraphState
|
||||
import com.tangem.utils.coroutines.JobHolder
|
||||
import com.tangem.utils.coroutines.saveIn
|
||||
|
|
@ -44,7 +45,11 @@ internal class SaveWalletMiddleware {
|
|||
is SaveWalletAction.AllowToUseBiometrics -> allowToUseBiometrics(state)
|
||||
is SaveWalletAction.EnrollBiometrics.Enroll -> enrollBiometrics()
|
||||
is SaveWalletAction.Dismiss -> dismiss(state)
|
||||
is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(state, action.hasBackupError)
|
||||
is SaveWalletAction.SaveWalletAfterBackup -> saveWalletAfterBackup(
|
||||
state = state,
|
||||
hasBackupError = action.hasBackupError,
|
||||
shouldNavigateToWallet = action.shouldNavigateToWallet,
|
||||
)
|
||||
is SaveWalletAction.AllowToUseBiometrics.Success,
|
||||
is SaveWalletAction.AllowToUseBiometrics.Error,
|
||||
is SaveWalletAction.ProvideBackupInfo,
|
||||
|
|
@ -55,7 +60,11 @@ internal class SaveWalletMiddleware {
|
|||
}
|
||||
}
|
||||
|
||||
private fun saveWalletAfterBackup(state: SaveWalletState, hasBackupError: Boolean) {
|
||||
private fun saveWalletAfterBackup(
|
||||
state: SaveWalletState,
|
||||
hasBackupError: Boolean,
|
||||
shouldNavigateToWallet: Boolean,
|
||||
) {
|
||||
scope.launch {
|
||||
val backupInfo = state.backupInfo ?: error("Backup info is null")
|
||||
|
||||
|
|
@ -77,8 +86,13 @@ internal class SaveWalletMiddleware {
|
|||
.doOnFailure { error ->
|
||||
Timber.e(error, "Unable to save user wallet")
|
||||
}
|
||||
.doOnSuccess { mainScope.launch { store.onUserWalletSelected(userWallet) } }
|
||||
.doOnResult { navigateToWallet() }
|
||||
.doOnSuccess {
|
||||
mainScope.launch { store.onUserWalletSelected(userWallet) }
|
||||
if (!shouldNavigateToWallet) {
|
||||
store.dispatch(OnboardingWalletAction.WalletSaved(userWallet.walletId))
|
||||
}
|
||||
}
|
||||
.doOnResult { if (shouldNavigateToWallet) navigateToWallet() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.data
|
||||
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.testnet.TestnetTokensStorage
|
||||
import com.tangem.domain.common.TapWorkarounds.isTestCard
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.features.tokens.impl.domain.TokensListRepository
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Default repository implementation of tokens list feature
|
||||
*
|
||||
* @property tangemTechApi Tangem Tech API
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property getSelectedWalletSyncUseCase use case that returns selected wallet
|
||||
* @property testnetTokensStorage storage for getting testnet tokens data
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultTokensListRepository(
|
||||
private val tangemTechApi: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val testnetTokensStorage: TestnetTokensStorage,
|
||||
) : TokensListRepository {
|
||||
|
||||
override fun getAvailableTokens(searchText: String?, needFilterExcluded: Boolean): Flow<PagingData<Token>> {
|
||||
return Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 100,
|
||||
prefetchDistance = 70,
|
||||
enablePlaceholders = false,
|
||||
),
|
||||
pagingSourceFactory = {
|
||||
val defaultSource = TangemApiTokensPagingSource(
|
||||
api = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
searchText = searchText,
|
||||
needFilterExcluded = needFilterExcluded,
|
||||
)
|
||||
|
||||
getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = { defaultSource },
|
||||
ifRight = {
|
||||
if (it.scanResponse.card.isTestCard) {
|
||||
TestnetTokensPagingSource(testnetTokensStorage, searchText)
|
||||
} else {
|
||||
defaultSource
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
).flow
|
||||
}
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.isSupportedInApp
|
||||
import com.tangem.blockchainsdk.utils.toNetworkId
|
||||
import com.tangem.datasource.api.common.response.getOrThrow
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.domain.common.extensions.supportedBlockchains
|
||||
import com.tangem.domain.common.util.cardTypesResolver
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.features.tokens.impl.data.converters.CoinsResponseConverter
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import com.tangem.utils.coroutines.runCatching
|
||||
|
||||
/**
|
||||
* Paging source that get tokens by Tangem Tech API
|
||||
*
|
||||
* @property api Tangem Tech API
|
||||
* @property dispatchers coroutine dispatchers provider
|
||||
* @property getSelectedWalletSyncUseCase use case that returns selected wallet
|
||||
* @property searchText search text
|
||||
* @property needFilterExcluded filter networks that are not supported in the app
|
||||
*/
|
||||
internal class TangemApiTokensPagingSource(
|
||||
private val api: TangemTechApi,
|
||||
private val dispatchers: CoroutineDispatcherProvider,
|
||||
private val getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
private val searchText: String?,
|
||||
private val needFilterExcluded: Boolean = false,
|
||||
) : PagingSource<Int, Token>() {
|
||||
|
||||
private val coinsResponseConverter = CoinsResponseConverter(needFilterExcluded)
|
||||
|
||||
private val allAvailableBlockchains by lazy {
|
||||
Blockchain.entries
|
||||
.filter {
|
||||
it.isTestnet().not() && (needFilterExcluded.not() || it.isSupportedInApp())
|
||||
}
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Token>): Int? {
|
||||
return state.anchorPosition?.let { anchorPosition ->
|
||||
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(other = 1)
|
||||
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(other = 1)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Token> {
|
||||
val page = params.key ?: 0
|
||||
|
||||
return runCatching(dispatchers.io) {
|
||||
val supportedBlockchains = getSelectedWalletSyncUseCase().fold(
|
||||
ifLeft = { allAvailableBlockchains },
|
||||
ifRight = { it.scanResponse.card.supportedBlockchains(it.scanResponse.cardTypesResolver) },
|
||||
)
|
||||
|
||||
api.getCoins(
|
||||
networkIds = supportedBlockchains.joinToString(separator = ",", transform = Blockchain::toNetworkId),
|
||||
active = true,
|
||||
searchText = searchText,
|
||||
offset = page * params.loadSize,
|
||||
limit = params.loadSize,
|
||||
).getOrThrow()
|
||||
}.fold(
|
||||
onSuccess = { response ->
|
||||
LoadResult.Page(
|
||||
data = coinsResponseConverter.convert(response),
|
||||
prevKey = if (page == 0) null else page.minus(other = 1),
|
||||
nextKey = if (response.coins.isEmpty()) null else page.plus(other = 1),
|
||||
)
|
||||
},
|
||||
onFailure = { LoadResult.Error(it) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.paging.PagingState
|
||||
import com.tangem.datasource.local.testnet.TestnetTokensStorage
|
||||
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
|
||||
import com.tangem.tap.features.tokens.impl.data.converters.TestnetTokensConfigConverter
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
|
||||
/**
|
||||
* Paging source that get tokens by testnet tokens config
|
||||
*
|
||||
* @property testnetTokensStorage testnet tokens config storage
|
||||
* @property searchText search text
|
||||
*/
|
||||
internal class TestnetTokensPagingSource(
|
||||
private val testnetTokensStorage: TestnetTokensStorage,
|
||||
private val searchText: String?,
|
||||
) : PagingSource<Int, Token>() {
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Token>): Int? = null
|
||||
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Token> {
|
||||
return LoadResult.Page(
|
||||
data = TestnetTokensConfigConverter.convert(
|
||||
value = testnetTokensStorage.getConfig().searchByText(searchText),
|
||||
),
|
||||
prevKey = null,
|
||||
nextKey = null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun TestnetTokensConfig.searchByText(searchText: String?): TestnetTokensConfig {
|
||||
if (searchText.isNullOrBlank()) return this
|
||||
|
||||
return copy(
|
||||
tokens = tokens.filter { token ->
|
||||
token.symbol.contains(other = searchText, ignoreCase = true) ||
|
||||
token.name.contains(other = searchText, ignoreCase = true)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.data.converters
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.blockchainsdk.utils.isSupportedInApp
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from data model [CoinsResponse] to list of domain models [Token]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class CoinsResponseConverter(val needFilterExcluded: Boolean) : Converter<CoinsResponse, List<Token>> {
|
||||
|
||||
override fun convert(value: CoinsResponse): List<Token> {
|
||||
return value.coins.map { token ->
|
||||
Token(
|
||||
id = token.id,
|
||||
name = token.name,
|
||||
symbol = token.symbol,
|
||||
iconUrl = getIconUrl(token.id, value.imageHost),
|
||||
networks = token.networks.mapNotNull { network ->
|
||||
val blockchain = Blockchain.fromNetworkId(network.networkId) ?: return@mapNotNull null
|
||||
|
||||
if (needFilterExcluded && !blockchain.isSupportedInApp()) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
// filter tokens, if contractAddress != null, assume that it is a token
|
||||
if (network.contractAddress != null &&
|
||||
!blockchain.canHandleTokens()
|
||||
) {
|
||||
return@mapNotNull null
|
||||
}
|
||||
|
||||
Token.Network(
|
||||
id = network.networkId,
|
||||
blockchain = blockchain,
|
||||
address = network.contractAddress,
|
||||
iconUrl = getIconUrl(network.networkId, value.imageHost),
|
||||
decimalCount = network.decimalCount?.toInt(),
|
||||
)
|
||||
},
|
||||
)
|
||||
}.filter { it.networks.isNotEmpty() }
|
||||
}
|
||||
|
||||
fun getIconUrl(id: String, imageHost: String? = null): String {
|
||||
return "${imageHost ?: DEFAULT_IMAGE_HOST}large/$id.png"
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val DEFAULT_IMAGE_HOST = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.data.converters
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchainsdk.utils.fromNetworkId
|
||||
import com.tangem.datasource.local.testnet.models.TestnetTokensConfig
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import com.tangem.utils.converter.Converter
|
||||
|
||||
/**
|
||||
* Converter from data model [TestnetTokensConfig] to list of domain models [Token]
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal object TestnetTokensConfigConverter : Converter<TestnetTokensConfig, List<Token>> {
|
||||
|
||||
private val coinsResponseConverter = CoinsResponseConverter(needFilterExcluded = false)
|
||||
override fun convert(value: TestnetTokensConfig): List<Token> {
|
||||
return value.tokens.map { token ->
|
||||
Token(
|
||||
id = token.id,
|
||||
name = token.name,
|
||||
symbol = token.symbol,
|
||||
iconUrl = coinsResponseConverter.getIconUrl(token.id),
|
||||
networks = token.networks?.mapNotNull { network ->
|
||||
val blockchain = Blockchain.fromNetworkId(network.id) ?: return@mapNotNull null
|
||||
|
||||
Token.Network(
|
||||
id = network.id,
|
||||
blockchain = blockchain,
|
||||
address = network.address,
|
||||
iconUrl = coinsResponseConverter.getIconUrl(network.id),
|
||||
decimalCount = network.decimalCount,
|
||||
)
|
||||
}.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.di
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.testnet.TestnetTokensStorage
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository
|
||||
import com.tangem.tap.features.tokens.impl.domain.DefaultTokensListInteractor
|
||||
import com.tangem.tap.features.tokens.impl.domain.TokensListInteractor
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
internal object TokensListInteractorModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideTokensListInteractor(
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
testnetTokensStorage: TestnetTokensStorage,
|
||||
): TokensListInteractor {
|
||||
return DefaultTokensListInteractor(
|
||||
repository = DefaultTokensListRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
testnetTokensStorage = testnetTokensStorage,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.di
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.local.testnet.TestnetTokensStorage
|
||||
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
|
||||
import com.tangem.tap.features.tokens.impl.data.DefaultTokensListRepository
|
||||
import com.tangem.tap.features.tokens.impl.domain.TokensListRepository
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
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 TokensListRepositoryModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesTokensListRepository(
|
||||
tangemTechApi: TangemTechApi,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
getSelectedWalletSyncUseCase: GetSelectedWalletSyncUseCase,
|
||||
testnetTokensStorage: TestnetTokensStorage,
|
||||
): TokensListRepository {
|
||||
return DefaultTokensListRepository(
|
||||
tangemTechApi = tangemTechApi,
|
||||
dispatchers = dispatchers,
|
||||
getSelectedWalletSyncUseCase = getSelectedWalletSyncUseCase,
|
||||
testnetTokensStorage = testnetTokensStorage,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.di
|
||||
|
||||
import com.tangem.tap.features.tokens.impl.presentation.router.DefaultTokensListRouter
|
||||
import com.tangem.tap.features.tokens.impl.presentation.router.TokensListRouter
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
import dagger.hilt.android.scopes.ViewModelScoped
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
internal object TokensListRouterModule {
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideTokensListRouter(): TokensListRouter = DefaultTokensListRouter()
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.domain
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Default implementation of tokens list interactor
|
||||
*
|
||||
* @property repository repository of tokens list feature
|
||||
*/
|
||||
internal class DefaultTokensListInteractor(private val repository: TokensListRepository) : TokensListInteractor {
|
||||
|
||||
override fun getTokensList(searchText: String, needFilterExcluded: Boolean): Flow<PagingData<Token>> {
|
||||
return repository.getAvailableTokens(
|
||||
searchText = searchText.ifBlank(defaultValue = { null }),
|
||||
needFilterExcluded = needFilterExcluded,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.domain
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Interactor of tokens list feature
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal interface TokensListInteractor {
|
||||
|
||||
/** Get tokens list using filter by text [searchText] */
|
||||
fun getTokensList(searchText: String, needFilterExcluded: Boolean): Flow<PagingData<Token>>
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.domain
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import com.tangem.tap.features.tokens.impl.domain.models.Token
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* Repository of tokens list feature
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal interface TokensListRepository {
|
||||
|
||||
/**
|
||||
* Get available tokens list
|
||||
*
|
||||
* @param searchText search text
|
||||
* @param needFilterExcluded flag that determines if excluded tokens should be filtered
|
||||
*
|
||||
* @throws com.tangem.datasource.api.common.response.ApiResponseError
|
||||
*/
|
||||
fun getAvailableTokens(searchText: String?, needFilterExcluded: Boolean): Flow<PagingData<Token>>
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.domain.models
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
||||
/**
|
||||
* Domain model of token for tokens list screen
|
||||
*
|
||||
* @property id token id
|
||||
* @property name token name
|
||||
* @property symbol token brief name. Example, "BTC"
|
||||
* @property iconUrl token icon url
|
||||
* @property networks token networks
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal data class Token(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
val iconUrl: String,
|
||||
val networks: List<Network>,
|
||||
) {
|
||||
|
||||
/**
|
||||
* Domain model of network for tokens list screen
|
||||
*
|
||||
* @property id network id
|
||||
* @property blockchain blockchain
|
||||
* @property address address. If address equals null, it means it is the main network of token
|
||||
* @property iconUrl network icon url
|
||||
* @property decimalCount decimal count
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
data class Network(
|
||||
val id: String,
|
||||
val blockchain: Blockchain,
|
||||
val address: String?,
|
||||
val iconUrl: String,
|
||||
val decimalCount: Int?,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.tangem.core.ui.UiDependencies
|
||||
import com.tangem.core.ui.screen.ComposeFragment
|
||||
import com.tangem.tap.features.tokens.impl.presentation.ui.TokensListScreen
|
||||
import com.tangem.tap.features.tokens.impl.presentation.viewmodels.TokensListViewModel
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Fragment with list of tokens
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@AndroidEntryPoint
|
||||
internal class TokensListFragment : ComposeFragment() {
|
||||
|
||||
@Inject
|
||||
override lateinit var uiDependencies: UiDependencies
|
||||
|
||||
@Composable
|
||||
override fun ScreenContent(modifier: Modifier) {
|
||||
val viewModel = hiltViewModel<TokensListViewModel>().apply {
|
||||
LocalLifecycleOwner.current.lifecycle.addObserver(this)
|
||||
}
|
||||
TokensListScreen(
|
||||
stateHolder = viewModel.uiState,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.models
|
||||
|
||||
/**
|
||||
* State that shows is token support by given card
|
||||
*/
|
||||
sealed class SupportTokensState {
|
||||
|
||||
object NetworkTokensUnsupported : SupportTokensState()
|
||||
object UnsupportedCurve : SupportTokensState()
|
||||
object SupportedToken : SupportTokensState()
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.router
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.common.routing.AppRoute
|
||||
import com.tangem.common.routing.AppRouter
|
||||
|
||||
import com.tangem.tap.common.extensions.dispatchDialogShow
|
||||
import com.tangem.tap.common.extensions.dispatchNavigationAction
|
||||
import com.tangem.tap.common.extensions.dispatchNotification
|
||||
import com.tangem.tap.common.redux.AppDialog
|
||||
import com.tangem.tap.store
|
||||
import com.tangem.wallet.R
|
||||
|
||||
/**
|
||||
* Default implementation of tokens list router
|
||||
* FIXME("Necessary to avoid using redux actions")
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
internal class DefaultTokensListRouter : TokensListRouter {
|
||||
|
||||
override fun popBackStack() {
|
||||
store.dispatchNavigationAction(AppRouter::pop)
|
||||
}
|
||||
|
||||
override fun openAddCustomTokenScreen() {
|
||||
store.dispatchNavigationAction { push(AppRoute.AddCustomToken) }
|
||||
}
|
||||
|
||||
override fun showAddressCopiedNotification() {
|
||||
store.dispatchNotification(R.string.contract_address_copied_message)
|
||||
}
|
||||
|
||||
override fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String) {
|
||||
store.dispatchDialogShow(
|
||||
dialog = AppDialog.TokensAreLinkedDialog(
|
||||
currencyTitle = tokenName,
|
||||
currencySymbol = tokenSymbol,
|
||||
networkName = networkName,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
override fun openRemoveWalletAlert(tokenName: String, onOkClick: () -> Unit) {
|
||||
store.dispatchDialogShow(
|
||||
dialog = AppDialog.RemoveWalletDialog(currencyTitle = tokenName, onOk = onOkClick),
|
||||
)
|
||||
}
|
||||
|
||||
override fun openUnsupportedNetworkAlert(blockchain: Blockchain) {
|
||||
val alert = AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_warning,
|
||||
messageId = R.string.alert_manage_tokens_unsupported_curve_message,
|
||||
args = listOf(blockchain.getNetworkName()),
|
||||
)
|
||||
store.dispatchDialogShow(alert)
|
||||
}
|
||||
|
||||
override fun showGenericErrorAlertAndPopBack() {
|
||||
val alert = AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_error,
|
||||
messageId = R.string.common_unknown_error,
|
||||
onOk = { store.dispatchNavigationAction(AppRouter::pop) },
|
||||
)
|
||||
store.dispatchDialogShow(alert)
|
||||
}
|
||||
|
||||
override fun openNetworkTokensNotSupportAlert(networkName: String) {
|
||||
store.dispatchDialogShow(
|
||||
AppDialog.SimpleOkDialogRes(
|
||||
headerId = R.string.common_warning,
|
||||
messageId = R.string.alert_manage_tokens_unsupported_message,
|
||||
args = listOf(networkName),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.router
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
|
||||
/** Tokens list feature router */
|
||||
internal interface TokensListRouter {
|
||||
|
||||
/** Return to last screen */
|
||||
fun popBackStack()
|
||||
|
||||
/** Open adding custom token screen */
|
||||
fun openAddCustomTokenScreen()
|
||||
|
||||
/** Show notification to inform about copied address */
|
||||
fun showAddressCopiedNotification()
|
||||
|
||||
/**
|
||||
* Open alert if unable to hide the main token
|
||||
*
|
||||
* @param tokenName token name
|
||||
* @param tokenSymbol token brief name
|
||||
* @param networkName blockchain network full name
|
||||
*/
|
||||
fun openUnableHideMainTokenAlert(tokenName: String, tokenSymbol: String, networkName: String)
|
||||
|
||||
/**
|
||||
* Open alert to remove wallet
|
||||
*
|
||||
* @param tokenName token name
|
||||
* @param onOkClick lambda be invoked if OkButton is been clicked
|
||||
*/
|
||||
fun openRemoveWalletAlert(tokenName: String, onOkClick: () -> Unit)
|
||||
|
||||
/** Open alert if solana network is unsupported
|
||||
*
|
||||
* @param blockchain blockchain to show alert
|
||||
*/
|
||||
fun openUnsupportedNetworkAlert(blockchain: Blockchain)
|
||||
|
||||
fun showGenericErrorAlertAndPopBack()
|
||||
|
||||
/**
|
||||
* Open alert with unsupported networks tokens error
|
||||
*/
|
||||
fun openNetworkTokensNotSupportAlert(networkName: String)
|
||||
}
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.states
|
||||
|
||||
import androidx.compose.runtime.MutableState
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.core.ui.extensions.getActiveIconRes
|
||||
import com.tangem.core.ui.extensions.getGreyedOutIconRes
|
||||
|
||||
/**
|
||||
* Network item state
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
sealed interface NetworkItemState {
|
||||
|
||||
/** Network name */
|
||||
val name: String
|
||||
|
||||
/** Network protocol name */
|
||||
val protocolName: String
|
||||
|
||||
/** Network icon id from resources */
|
||||
val iconResId: MutableState<Int>
|
||||
|
||||
/** Flag that determines if the network is the main network for the token */
|
||||
val isMainNetwork: Boolean
|
||||
|
||||
/**
|
||||
* Network item state that is available for read
|
||||
*
|
||||
* @property name network name
|
||||
* @property protocolName network protocol name
|
||||
* @property iconResId network icon id from resources
|
||||
* @property isMainNetwork flag that determines if the network is the main network for the token
|
||||
*/
|
||||
data class ReadContent(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
override val iconResId: MutableState<Int>,
|
||||
override val isMainNetwork: Boolean,
|
||||
) : NetworkItemState
|
||||
|
||||
/**
|
||||
* Network item state that is available for read and edit
|
||||
*
|
||||
* @property name network name
|
||||
* @property protocolName network protocol name
|
||||
* @property iconResId network icon id from resources
|
||||
* @property isMainNetwork flag that determines if the network is the main network for the token
|
||||
* @property isAdded flag that determines if the user has saved the token
|
||||
* @property id network id
|
||||
* @property address contract address
|
||||
* @property decimalCount decimal count
|
||||
* @property blockchain blockchain
|
||||
* @property onToggleClick lambda be invoked when switch is been toggled
|
||||
* @property onNetworkClick lambda be invoked when network item is been clicked
|
||||
*/
|
||||
data class ManageContent(
|
||||
override val name: String,
|
||||
override val protocolName: String,
|
||||
override val iconResId: MutableState<Int>,
|
||||
override val isMainNetwork: Boolean,
|
||||
val isAdded: MutableState<Boolean>,
|
||||
val id: String,
|
||||
val address: String?,
|
||||
val decimalCount: Int?,
|
||||
val blockchain: Blockchain,
|
||||
val onToggleClick: (TokenItemState.ManageContent, ManageContent) -> Unit,
|
||||
val onNetworkClick: () -> Unit,
|
||||
) : NetworkItemState {
|
||||
|
||||
/**
|
||||
* Change toggle state [isAdded].
|
||||
*
|
||||
* It is a hack that helps us to change element of flow
|
||||
*/
|
||||
fun changeToggleState() {
|
||||
val reverseState = !isAdded.value
|
||||
isAdded.value = reverseState
|
||||
iconResId.value = if (reverseState) getActiveIconRes(blockchain.id) else getGreyedOutIconRes(blockchain.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.states
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
|
||||
/**
|
||||
* Token item state.
|
||||
* All subclasses is stable, but @Immutable annotation is required to use this sealed class like as
|
||||
* field of TokensListStateHolder.
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface TokenItemState {
|
||||
|
||||
/** Token id */
|
||||
val id: String
|
||||
|
||||
/** Token full name (name with symbol) */
|
||||
val fullName: String
|
||||
|
||||
/** Token icon url */
|
||||
val iconUrl: String
|
||||
|
||||
/** List of networks */
|
||||
val networks: ImmutableList<NetworkItemState>
|
||||
|
||||
/** Token composed id that unique for tokens in different networks */
|
||||
val composedId: String
|
||||
|
||||
/**
|
||||
* Token item state that is available for read
|
||||
*
|
||||
* @property id token id
|
||||
* @property fullName token name
|
||||
* @property iconUrl token icon url
|
||||
* @property networks list of networks that is available for read
|
||||
* @property composedId token composed id to use in lists
|
||||
*/
|
||||
data class ReadContent(
|
||||
override val id: String,
|
||||
override val fullName: String,
|
||||
override val iconUrl: String,
|
||||
override val networks: ImmutableList<NetworkItemState.ReadContent>,
|
||||
override val composedId: String,
|
||||
) : 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 composedId token composed id to use in lists
|
||||
* @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>,
|
||||
override val composedId: String,
|
||||
val name: String,
|
||||
val symbol: String,
|
||||
) : TokenItemState
|
||||
}
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.states
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.PagingData
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
/**
|
||||
* State holder for screen with list of tokens
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Immutable
|
||||
internal sealed interface TokensListStateHolder {
|
||||
|
||||
/** Toolbar state */
|
||||
val toolbarState: TokensListToolbarState
|
||||
|
||||
/** Loading state */
|
||||
val isLoading: Boolean
|
||||
|
||||
/** Flag that determines if warning block is visible */
|
||||
val isDifferentAddressesBlockVisible: Boolean
|
||||
|
||||
/** Tokens list */
|
||||
val tokens: Flow<PagingData<TokenItemState>>
|
||||
|
||||
/** Callback to be invoked when [tokens] loading state is been changed */
|
||||
val onTokensLoadStateChanged: (LoadState) -> Unit
|
||||
|
||||
/**
|
||||
* Util function that allow to make a copy
|
||||
*
|
||||
* @param toolbarState toolbar state
|
||||
* @param isLoading loading state
|
||||
* @param isDifferentAddressesBlockVisible flag that determines if warning block is visible
|
||||
* @param tokens tokens list
|
||||
* @param onTokensLoadStateChanged callback to be invoked when tokens loading state is been changed
|
||||
*/
|
||||
fun copySealed(
|
||||
toolbarState: TokensListToolbarState = this.toolbarState,
|
||||
isLoading: Boolean = this.isLoading,
|
||||
isDifferentAddressesBlockVisible: Boolean = this.isDifferentAddressesBlockVisible,
|
||||
tokens: Flow<PagingData<TokenItemState>> = this.tokens,
|
||||
onTokensLoadStateChanged: (LoadState) -> Unit = this.onTokensLoadStateChanged,
|
||||
): TokensListStateHolder {
|
||||
return when (this) {
|
||||
is ManageContent -> copy(
|
||||
toolbarState,
|
||||
isLoading,
|
||||
isDifferentAddressesBlockVisible,
|
||||
tokens,
|
||||
onTokensLoadStateChanged,
|
||||
)
|
||||
is ReadContent -> copy(
|
||||
toolbarState,
|
||||
isLoading,
|
||||
isDifferentAddressesBlockVisible,
|
||||
tokens,
|
||||
onTokensLoadStateChanged,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* State screen that is available only for read
|
||||
*
|
||||
* @property toolbarState toolbar state
|
||||
* @property isLoading loading state
|
||||
* @property isDifferentAddressesBlockVisible flag that determines if warning block is visible
|
||||
* @property tokens tokens list
|
||||
* @property onTokensLoadStateChanged callback to be invoked when tokens loading state is been changed
|
||||
*/
|
||||
data class ReadContent(
|
||||
override val toolbarState: TokensListToolbarState,
|
||||
override val isLoading: Boolean,
|
||||
override val isDifferentAddressesBlockVisible: Boolean,
|
||||
override val tokens: Flow<PagingData<TokenItemState>>,
|
||||
override val onTokensLoadStateChanged: (LoadState) -> Unit,
|
||||
) : TokensListStateHolder
|
||||
|
||||
/**
|
||||
* State screen that is available for read and manage
|
||||
*
|
||||
* @property toolbarState toolbar state
|
||||
* @property isLoading loading state
|
||||
* @property isDifferentAddressesBlockVisible flag that determines if warning block is visible
|
||||
* @property tokens tokens list
|
||||
* @property onTokensLoadStateChanged callback to be invoked when tokens loading state is been changed
|
||||
* @property onSaveButtonClick callback to be invoked when SaveButton is being clicked
|
||||
*/
|
||||
data class ManageContent(
|
||||
override val toolbarState: TokensListToolbarState,
|
||||
override val isLoading: Boolean,
|
||||
override val isDifferentAddressesBlockVisible: Boolean,
|
||||
override val tokens: Flow<PagingData<TokenItemState>>,
|
||||
override val onTokensLoadStateChanged: (LoadState) -> Unit,
|
||||
val onSaveButtonClick: () -> Unit,
|
||||
val isSavingInProgress: Boolean,
|
||||
) : TokensListStateHolder
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.states
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
|
||||
/**
|
||||
* Toolbar state.
|
||||
* All subclasses is stable, but @Immutable annotation is required to use this sealed class like as
|
||||
* field of TokensListStateHolder.
|
||||
*/
|
||||
@Immutable
|
||||
sealed interface TokensListToolbarState {
|
||||
|
||||
/** Callback to be invoked when BackButton is being clicked */
|
||||
val onBackButtonClick: () -> Unit
|
||||
|
||||
/** Toolbar state as title */
|
||||
sealed interface Title : TokensListToolbarState {
|
||||
|
||||
/** Toolbar title id from resources */
|
||||
val titleResId: Int
|
||||
|
||||
/** Callback to be invoked when SearchButton is being clicked */
|
||||
val onSearchButtonClick: () -> Unit
|
||||
|
||||
/**
|
||||
* Title state that is available only for read
|
||||
*
|
||||
* @property onBackButtonClick callback to be invoked when BackButton is being clicked
|
||||
* @property titleResId toolbar title id from resources
|
||||
* @property onSearchButtonClick callback to be invoked when SearchButton is being clicked
|
||||
*/
|
||||
data class Read(
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
override val titleResId: Int,
|
||||
override val onSearchButtonClick: () -> Unit,
|
||||
) : Title
|
||||
|
||||
/**
|
||||
* Title state that is available for read and manage
|
||||
*
|
||||
* @property onBackButtonClick callback to be invoked when BackButton is being clicked
|
||||
* @property titleResId toolbar title id from resources
|
||||
* @property onSearchButtonClick callback to be invoked when SearchButton is being clicked
|
||||
* @property onAddCustomTokenClick callback to be invoked when AddCustomTokenButton is being clicked
|
||||
*/
|
||||
data class Manage(
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
override val titleResId: Int,
|
||||
override val onSearchButtonClick: () -> Unit,
|
||||
val onAddCustomTokenClick: () -> Unit,
|
||||
) : Title
|
||||
}
|
||||
|
||||
/**
|
||||
* Toolbar state as input field
|
||||
*
|
||||
* @property onBackButtonClick callback to be invoked when BackButton is being clicked
|
||||
* @property value input value
|
||||
* @property onValueChange lambda to be invoked when search value is being changed
|
||||
* @property onCleanButtonClick callback to be invoked when CleanButton is being clicked
|
||||
*/
|
||||
data class InputField(
|
||||
override val onBackButtonClick: () -> Unit,
|
||||
val value: String,
|
||||
val onValueChange: (String) -> Unit,
|
||||
val onCleanButtonClick: () -> Unit,
|
||||
) : TokensListToolbarState
|
||||
}
|
||||
|
|
@ -1,144 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
|
||||
import kotlinx.collections.immutable.ImmutableCollection
|
||||
|
||||
/** This const configures how many items show to user and hide more than*/
|
||||
private const val MAX_VISIBLE_BRIEF_ICONS = 9
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun BriefNetworksList(
|
||||
isCollapsed: Boolean,
|
||||
networks: ImmutableCollection<NetworkItemState>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isCollapsed,
|
||||
modifier = modifier,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing4)) {
|
||||
for ((index, network) in networks.withIndex()) {
|
||||
if (index < MAX_VISIBLE_BRIEF_ICONS) {
|
||||
key(network.name + network.protocolName) {
|
||||
BriefNetworkItem(model = network)
|
||||
}
|
||||
} else {
|
||||
if (networks.size < MAX_VISIBLE_BRIEF_ICONS + 1) {
|
||||
key(network.name + network.protocolName) {
|
||||
BriefNetworkItem(model = network)
|
||||
}
|
||||
} else {
|
||||
HasMoreItem(moreCount = networks.size - index)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun BriefNetworkItem(model: NetworkItemState, modifier: Modifier = Modifier) {
|
||||
val isAdded = model is NetworkItemState.ManageContent && model.isAdded.value
|
||||
Box(modifier = modifier.size(size = TangemTheme.dimens.size20)) {
|
||||
if (!isAdded) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size20)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.control.unchecked),
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
painter = painterResource(id = model.iconResId.value),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size20)
|
||||
.clip(CircleShape),
|
||||
tint = if (isAdded) Color.Unspecified else TangemTheme.colors.text.tertiary,
|
||||
)
|
||||
|
||||
if (model.isMainNetwork) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopEnd)
|
||||
.size(TangemTheme.dimens.size7)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.background.primary),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size5)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.icon.accent),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
@Composable
|
||||
internal fun HasMoreItem(moreCount: Int) {
|
||||
val count = if (moreCount > 99) 99 else moreCount
|
||||
val themeTextStyle = TangemTheme.typography.overline.copy(
|
||||
letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp),
|
||||
)
|
||||
var textStyle by remember(themeTextStyle) { mutableStateOf(themeTextStyle) }
|
||||
var readyToDraw by remember(themeTextStyle) { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size20)
|
||||
.clip(CircleShape)
|
||||
.background(TangemTheme.colors.control.unchecked),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.drawWithContent { if (readyToDraw) drawContent() },
|
||||
text = "+$count",
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
overflow = TextOverflow.Clip,
|
||||
onTextLayout = { textLayoutResult ->
|
||||
if (textLayoutResult.hasVisualOverflow) {
|
||||
textStyle = textStyle.copy(fontSize = textStyle.fontSize * 0.9)
|
||||
} else {
|
||||
readyToDraw = true
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,163 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
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.*
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.SwitchDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
|
||||
import kotlinx.collections.immutable.ImmutableCollection
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun DetailedNetworksList(
|
||||
isExpanded: Boolean,
|
||||
token: TokenItemState,
|
||||
networks: ImmutableCollection<NetworkItemState>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = modifier,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
Column {
|
||||
networks.forEachIndexed { index, network ->
|
||||
key(network.name + network.protocolName) {
|
||||
DetailedNetworkItem(token = token, network = network, isLastItem = networks.size - 1 == index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun DetailedNetworkItem(token: TokenItemState, network: NetworkItemState, isLastItem: Boolean) {
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val itemHeight = TangemTheme.dimens.size50
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
enabled = network is NetworkItemState.ManageContent,
|
||||
onLongClick = {
|
||||
if (network is NetworkItemState.ManageContent && network.address != null) {
|
||||
clipboardManager.setText(AnnotatedString(text = network.address))
|
||||
network.onNetworkClick()
|
||||
}
|
||||
},
|
||||
onClick = {},
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.heightIn(min = itemHeight)
|
||||
.padding(start = TangemTheme.dimens.spacing38),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
NetworkItemArrow(itemHeight = itemHeight, isLastItem = isLastItem)
|
||||
Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing16))
|
||||
BriefNetworkItem(model = network)
|
||||
Spacer(modifier = Modifier.width(TangemTheme.dimens.spacing6))
|
||||
NetworkTitle(model = network)
|
||||
|
||||
if (network is NetworkItemState.ManageContent) {
|
||||
Switch(
|
||||
checked = network.isAdded.value,
|
||||
onCheckedChange = {
|
||||
network.onToggleClick(
|
||||
requireNotNull(token as? TokenItemState.ManageContent),
|
||||
network,
|
||||
)
|
||||
},
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing16, end = TangemTheme.dimens.spacing8),
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = TangemTheme.colors.control.key,
|
||||
checkedTrackColor = TangemTheme.colors.icon.accent,
|
||||
uncheckedThumbColor = TangemTheme.colors.control.key,
|
||||
uncheckedTrackColor = TangemTheme.colors.icon.informative,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.NetworkTitle(model: NetworkItemState) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = buildAnnotatedString {
|
||||
append(text = model.name)
|
||||
append(text = " ")
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = if (model.isMainNetwork) {
|
||||
TangemTheme.colors.icon.accent
|
||||
} else {
|
||||
TangemTheme.colors.icon.secondary
|
||||
},
|
||||
),
|
||||
) {
|
||||
append(text = model.protocolName)
|
||||
}
|
||||
},
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 13.sp,
|
||||
color = if (model is NetworkItemState.ManageContent && model.isAdded.value) {
|
||||
TangemTheme.colors.text.primary1
|
||||
} else {
|
||||
TangemTheme.colors.text.secondary
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_DetailedNetworksList_ManageAccess() {
|
||||
TangemThemePreview {
|
||||
DetailedNetworksList(
|
||||
isExpanded = true,
|
||||
token = TokenListPreviewData.createManageToken(),
|
||||
networks = TokenListPreviewData.createManageNetworksList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_DetailedNetworksList_ReadAccess() {
|
||||
TangemThemePreview {
|
||||
DetailedNetworksList(
|
||||
isExpanded = true,
|
||||
token = TokenListPreviewData.createReadToken(),
|
||||
networks = TokenListPreviewData.createReadNetworksList(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.wallet.R
|
||||
|
||||
private const val SPECIAL_HEIGHT_3_5 = 3.5
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun NetworkItemArrow(itemHeight: Dp, isLastItem: Boolean) {
|
||||
Box(modifier = Modifier.height(height = itemHeight)) {
|
||||
if (!isLastItem) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing0_5)
|
||||
.background(color = TangemTheme.colors.stroke.primary)
|
||||
.size(width = TangemTheme.dimens.size1, height = itemHeight),
|
||||
)
|
||||
}
|
||||
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_link),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.height(height = itemHeight / 2 + SPECIAL_HEIGHT_3_5.dp),
|
||||
tint = TangemTheme.colors.stroke.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_NetworkItemArrow_Column() {
|
||||
TangemThemePreview {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(start = TangemTheme.dimens.size36),
|
||||
) {
|
||||
NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = false)
|
||||
NetworkItemArrow(itemHeight = TangemTheme.dimens.size62, isLastItem = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.ImageBackgroundContrastChecker
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
internal fun TokenItem(model: TokenItemState) {
|
||||
var isExpanded by rememberSaveable { mutableStateOf(value = false) }
|
||||
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
|
||||
|
||||
ConstraintLayout(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.padding(top = TangemTheme.dimens.spacing16),
|
||||
) {
|
||||
val (icon, title, availableNetworksText) = createRefs()
|
||||
val (briefNetworksList, detailedNetworksList, changeNetworksViewButton) = createRefs()
|
||||
|
||||
val spacing16 = TangemTheme.dimens.spacing16
|
||||
val spacing6 = TangemTheme.dimens.spacing6
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = iconBackgroundColor,
|
||||
shape = TangemTheme.shapes.roundedCorners8,
|
||||
)
|
||||
.size(TangemTheme.dimens.size46)
|
||||
.constrainAs(icon) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(anchor = parent.start, margin = spacing16)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
name = model.fullName,
|
||||
iconUrl = model.iconUrl,
|
||||
onContrastCalculate = { iconBackgroundColor = it },
|
||||
)
|
||||
}
|
||||
|
||||
Title(
|
||||
title = model.fullName,
|
||||
modifier = Modifier.constrainAs(title) {
|
||||
top.linkTo(parent.top)
|
||||
start.linkTo(icon.end, margin = spacing16)
|
||||
end.linkTo(anchor = changeNetworksViewButton.start, margin = spacing16)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
|
||||
Subtitle(
|
||||
isExpanded = isExpanded,
|
||||
modifier = Modifier.constrainAs(availableNetworksText) {
|
||||
top.linkTo(title.bottom, margin = spacing6)
|
||||
start.linkTo(icon.end, margin = spacing16)
|
||||
end.linkTo(anchor = changeNetworksViewButton.start, margin = spacing16)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
|
||||
BriefNetworksList(
|
||||
isCollapsed = !isExpanded,
|
||||
networks = model.networks,
|
||||
modifier = Modifier.constrainAs(briefNetworksList) {
|
||||
top.linkTo(title.bottom, margin = spacing6)
|
||||
start.linkTo(icon.end, margin = spacing16)
|
||||
end.linkTo(anchor = changeNetworksViewButton.start, margin = spacing16)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
|
||||
DetailedNetworksList(
|
||||
isExpanded = isExpanded,
|
||||
token = model,
|
||||
networks = model.networks,
|
||||
modifier = Modifier.constrainAs(detailedNetworksList) {
|
||||
top.linkTo(anchor = availableNetworksText.bottom, margin = spacing16)
|
||||
centerHorizontallyTo(parent)
|
||||
width = Dimension.fillToConstraints
|
||||
},
|
||||
)
|
||||
|
||||
ChangeNetworksViewButton(
|
||||
isExpanded = isExpanded,
|
||||
onClick = { isExpanded = !isExpanded },
|
||||
modifier = Modifier.constrainAs(changeNetworksViewButton) {
|
||||
top.linkTo(parent.top)
|
||||
end.linkTo(anchor = parent.end, margin = spacing6)
|
||||
if (!isExpanded) bottom.linkTo(parent.bottom)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Icon(name: String, iconUrl: String, onContrastCalculate: (Color) -> Unit, modifier: Modifier = Modifier) {
|
||||
val pixelsSize = with(LocalDensity.current) { TangemTheme.dimens.size46.roundToPx() }
|
||||
val iconModifier = modifier
|
||||
.size(size = TangemTheme.dimens.size46)
|
||||
.clip(TangemTheme.shapes.roundedCorners8)
|
||||
|
||||
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
|
||||
var isBackgroundColorDefined by remember { mutableStateOf(false) }
|
||||
val itemBackgroundColor = TangemTheme.colors.background.primary.toArgb()
|
||||
val isDarkTheme = isSystemInDarkTheme()
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
SubcomposeAsyncImage(
|
||||
modifier = iconModifier,
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(data = iconUrl)
|
||||
.size(size = pixelsSize)
|
||||
.memoryCacheKey(key = iconUrl + pixelsSize)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(false)
|
||||
.listener(
|
||||
onSuccess = { _, result ->
|
||||
if (!isBackgroundColorDefined && isDarkTheme) {
|
||||
coroutineScope.launch {
|
||||
val color = ImageBackgroundContrastChecker(
|
||||
drawable = result.drawable,
|
||||
backgroundColor = itemBackgroundColor,
|
||||
size = pixelsSize,
|
||||
).getContrastColor(isDarkTheme = true)
|
||||
onContrastCalculate(color)
|
||||
iconBackgroundColor = color
|
||||
isBackgroundColorDefined = true
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
loading = { CurrencyPlaceholderIcon(id = name, modifier = iconModifier) },
|
||||
error = { CurrencyPlaceholderIcon(id = name, modifier = iconModifier) },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Title(title: String, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = modifier,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Start,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body1,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Subtitle(isExpanded: Boolean, modifier: Modifier = Modifier) {
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded,
|
||||
modifier = modifier,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = R.string.currency_subtitle_expanded),
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChangeNetworksViewButton(isExpanded: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
IconButton(onClick = onClick, modifier = modifier.size(size = TangemTheme.dimens.size46)) {
|
||||
AnimatedContent(
|
||||
targetState = isExpanded,
|
||||
transitionSpec = { (fadeIn() + scaleIn()).togetherWith(scaleOut() + fadeOut()) },
|
||||
) { isExpanded ->
|
||||
Icon(
|
||||
painter = painterResource(
|
||||
id = if (isExpanded) R.drawable.ic_arrow_extended else R.drawable.ic_arrow_collapsed,
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_TokenItem_ManageAccess() {
|
||||
TangemThemePreview {
|
||||
TokenItem(model = TokenListPreviewData.createManageToken())
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_TokenItem_ReadAccess() {
|
||||
TangemThemePreview {
|
||||
TokenItem(model = TokenListPreviewData.createReadToken())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.ui
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.NetworkItemState
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
object TokenListPreviewData {
|
||||
|
||||
fun createManageToken(): TokenItemState.ManageContent {
|
||||
return TokenItemState.ManageContent(
|
||||
fullName = "Tether (USDT)",
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png",
|
||||
networks = createManageNetworksList(),
|
||||
composedId = "11231",
|
||||
id = "",
|
||||
name = "Tether",
|
||||
symbol = "",
|
||||
)
|
||||
}
|
||||
|
||||
fun createReadToken(): TokenItemState.ReadContent {
|
||||
return TokenItemState.ReadContent(
|
||||
id = "1",
|
||||
fullName = "Tether (USDT)",
|
||||
iconUrl = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/tether.png",
|
||||
networks = createReadNetworksList(),
|
||||
composedId = "11231",
|
||||
)
|
||||
}
|
||||
|
||||
fun createManageNetworksList(): ImmutableList<NetworkItemState.ManageContent> {
|
||||
return persistentListOf(
|
||||
NetworkItemState.ManageContent(
|
||||
name = "Ethereum",
|
||||
protocolName = "MAIN",
|
||||
iconResId = mutableStateOf(R.drawable.ic_eth_16),
|
||||
isMainNetwork = true,
|
||||
isAdded = mutableStateOf(true),
|
||||
id = "",
|
||||
address = null,
|
||||
onToggleClick = { _, _ -> },
|
||||
onNetworkClick = {},
|
||||
decimalCount = null,
|
||||
blockchain = Blockchain.Ethereum,
|
||||
),
|
||||
NetworkItemState.ManageContent(
|
||||
name = "BNB SMART CHAIN",
|
||||
protocolName = "BEP20",
|
||||
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
|
||||
isMainNetwork = false,
|
||||
isAdded = mutableStateOf(false),
|
||||
id = "",
|
||||
address = null,
|
||||
onToggleClick = { _, _ -> },
|
||||
onNetworkClick = {},
|
||||
decimalCount = null,
|
||||
blockchain = Blockchain.BSC,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun createReadNetworksList(): ImmutableList<NetworkItemState.ReadContent> {
|
||||
return persistentListOf(
|
||||
NetworkItemState.ReadContent(
|
||||
name = "Ethereum",
|
||||
protocolName = "MAIN",
|
||||
iconResId = mutableStateOf(R.drawable.ic_eth_16),
|
||||
isMainNetwork = true,
|
||||
),
|
||||
NetworkItemState.ReadContent(
|
||||
name = "BNB SMART CHAIN",
|
||||
protocolName = "BEP20",
|
||||
iconResId = mutableStateOf(R.drawable.ic_bsc_16),
|
||||
isMainNetwork = false,
|
||||
),
|
||||
// check icon clipping
|
||||
NetworkItemState.ReadContent(
|
||||
name = "SHIBARIUM",
|
||||
protocolName = "BEP20",
|
||||
iconResId = mutableStateOf(R.drawable.ic_shibarium_22),
|
||||
isMainNetwork = false,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.ui
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.FabPosition
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.TextUnitType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.itemContentType
|
||||
import androidx.paging.compose.itemKey
|
||||
import com.tangem.core.ui.components.BottomFade
|
||||
import com.tangem.core.ui.components.NavigationBar3ButtonsScrim
|
||||
import com.tangem.core.ui.components.PrimaryButton
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokenItemState
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListStateHolder
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
/**
|
||||
* Tokens list screen
|
||||
*
|
||||
* @param stateHolder state holder
|
||||
*
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modifier = Modifier) {
|
||||
BackHandler(onBack = stateHolder.toolbarState.onBackButtonClick)
|
||||
|
||||
var floatingButtonHeight by remember { mutableStateOf(value = 0.dp) }
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
TokensListToolbar(state = stateHolder.toolbarState)
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (stateHolder is TokensListStateHolder.ManageContent) {
|
||||
val density = LocalDensity.current
|
||||
val verticalPadding = TangemTheme.dimens.spacing32
|
||||
|
||||
SaveChangesButton(
|
||||
modifier = Modifier
|
||||
.navigationBarsPadding()
|
||||
.onSizeChanged {
|
||||
with(density) { floatingButtonHeight = it.height.toDp() + verticalPadding }
|
||||
},
|
||||
showProgress = stateHolder.isSavingInProgress,
|
||||
onClick = stateHolder.onSaveButtonClick,
|
||||
)
|
||||
}
|
||||
},
|
||||
floatingActionButtonPosition = FabPosition.Center,
|
||||
backgroundColor = TangemTheme.colors.background.secondary,
|
||||
) { _ ->
|
||||
val tokens = stateHolder.tokens.collectAsLazyPagingItems()
|
||||
|
||||
if (stateHolder !is TokensListStateHolder.ManageContent) {
|
||||
NavigationBar3ButtonsScrim()
|
||||
}
|
||||
|
||||
Box {
|
||||
TokensListContent(
|
||||
isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible,
|
||||
tokens = tokens,
|
||||
bottomMarginDp = floatingButtonHeight,
|
||||
)
|
||||
|
||||
if (stateHolder is TokensListStateHolder.ManageContent) {
|
||||
BottomFade(Modifier.align(Alignment.BottomCenter))
|
||||
}
|
||||
}
|
||||
|
||||
Crossfade(targetState = stateHolder.isLoading, label = "Update progress bar visibility") {
|
||||
if (it) {
|
||||
LoadingContent()
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(key1 = tokens.loadState.refresh) {
|
||||
stateHolder.onTokensLoadStateChanged(tokens.loadState.refresh)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LoadingContent() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(color = TangemTheme.colors.background.primary),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(color = TangemTheme.colors.icon.accent)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TokensListContent(
|
||||
isDifferentAddressesBlockVisible: Boolean,
|
||||
tokens: LazyPagingItems<TokenItemState>,
|
||||
bottomMarginDp: Dp,
|
||||
) {
|
||||
val state = rememberLazyListState()
|
||||
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.background(color = TangemTheme.colors.background.primary)
|
||||
.imePadding()
|
||||
.fillMaxSize(),
|
||||
state = state,
|
||||
contentPadding = PaddingValues(
|
||||
bottom = bottomMarginDp + bottomBarHeight,
|
||||
),
|
||||
) {
|
||||
item(
|
||||
key = "DifferentAddressesWarning$isDifferentAddressesBlockVisible",
|
||||
contentType = "DifferentAddressesWarning$isDifferentAddressesBlockVisible",
|
||||
) {
|
||||
if (isDifferentAddressesBlockVisible) DifferentAddressesWarning()
|
||||
}
|
||||
|
||||
tokens.itemKey(TokenItemState::composedId)
|
||||
tokens.itemContentType(TokenItemState::composedId)
|
||||
|
||||
items(items = tokens.itemSnapshotList.items, key = TokenItemState::composedId) {
|
||||
TokenItem(model = it)
|
||||
}
|
||||
}
|
||||
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(key1 = state) {
|
||||
snapshotFlow(state::isScrollInProgress)
|
||||
.distinctUntilChanged()
|
||||
.collectLatest { if (it) keyboardController?.hide() }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DifferentAddressesWarning() {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing16)
|
||||
.background(
|
||||
color = TangemTheme.colors.button.disabled,
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius10),
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
val text = stringResource(id = R.string.warning_manage_tokens_legacy_derivation_message)
|
||||
Text(
|
||||
text = text,
|
||||
modifier = Modifier.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing8,
|
||||
),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
textAlign = TextAlign.Start,
|
||||
style = TangemTheme.typography.body2.copy(
|
||||
letterSpacing = TextUnit(value = 0.5f, type = TextUnitType.Sp),
|
||||
lineHeight = TextUnit(value = 25f, type = TextUnitType.Sp),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SaveChangesButton(showProgress: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
PrimaryButton(
|
||||
modifier = modifier
|
||||
.imePadding()
|
||||
.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
.fillMaxWidth(),
|
||||
text = stringResource(id = R.string.common_save_changes),
|
||||
showProgress = showProgress,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showSystemUi = true)
|
||||
@Preview(showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_TokensListScreen(
|
||||
@PreviewParameter(TokensListScreenProvider::class) stateHolder: TokensListStateHolder,
|
||||
) {
|
||||
TangemThemePreview {
|
||||
TokensListScreen(stateHolder)
|
||||
}
|
||||
}
|
||||
|
||||
private class TokensListScreenProvider : CollectionPreviewParameterProvider<TokensListStateHolder>(
|
||||
collection = listOf(
|
||||
TokensListStateHolder.ReadContent(
|
||||
toolbarState = TokensListToolbarState.Title.Manage(
|
||||
titleResId = R.string.main_manage_tokens,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
onAddCustomTokenClick = {},
|
||||
),
|
||||
isLoading = true,
|
||||
isDifferentAddressesBlockVisible = false,
|
||||
tokens = emptyFlow(),
|
||||
onTokensLoadStateChanged = {},
|
||||
),
|
||||
TokensListStateHolder.ManageContent(
|
||||
toolbarState = TokensListToolbarState.Title.Manage(
|
||||
titleResId = R.string.main_manage_tokens,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
onAddCustomTokenClick = {},
|
||||
),
|
||||
isLoading = false,
|
||||
isDifferentAddressesBlockVisible = true,
|
||||
tokens = flowOf(
|
||||
PagingData.from(
|
||||
listOf(TokenListPreviewData.createManageToken()),
|
||||
),
|
||||
),
|
||||
onSaveButtonClick = {},
|
||||
onTokensLoadStateChanged = {},
|
||||
isSavingInProgress = false,
|
||||
),
|
||||
TokensListStateHolder.ReadContent(
|
||||
toolbarState = TokensListToolbarState.Title.Read(
|
||||
titleResId = R.string.common_search_tokens,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
),
|
||||
isLoading = false,
|
||||
isDifferentAddressesBlockVisible = false,
|
||||
tokens = flowOf(
|
||||
PagingData.from(
|
||||
listOf(TokenListPreviewData.createManageToken()),
|
||||
),
|
||||
),
|
||||
onTokensLoadStateChanged = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -1,221 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.ui
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreview
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState.InputField
|
||||
import com.tangem.tap.features.tokens.impl.presentation.states.TokensListToolbarState.Title
|
||||
import com.tangem.wallet.R
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
@Composable
|
||||
internal fun TokensListToolbar(state: TokensListToolbarState) {
|
||||
val toolbarElevation = if (isSystemInDarkTheme()) {
|
||||
TangemTheme.dimens.elevation0
|
||||
} else {
|
||||
AppBarDefaults.TopAppBarElevation
|
||||
}
|
||||
val statusBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getTop(this).toDp() }
|
||||
|
||||
TopAppBar(
|
||||
backgroundColor = TangemTheme.colors.background.secondary,
|
||||
contentPadding = PaddingValues(
|
||||
start = TangemTheme.dimens.spacing4,
|
||||
end = TangemTheme.dimens.spacing4,
|
||||
top = statusBarHeight,
|
||||
),
|
||||
elevation = toolbarElevation,
|
||||
) {
|
||||
IconButton(onClick = state.onBackButtonClick) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_back_24),
|
||||
contentDescription = "Go back",
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
}
|
||||
|
||||
when (state) {
|
||||
is Title -> TitleContent(state = state, modifier = Modifier.weight(1f))
|
||||
is InputField -> InputContent(state = state, modifier = Modifier.weight(1f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TitleContent(state: Title, modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = stringResource(id = state.titleResId),
|
||||
modifier = modifier.padding(horizontal = TangemTheme.dimens.spacing26),
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
maxLines = 1,
|
||||
style = TangemTheme.typography.h3,
|
||||
)
|
||||
|
||||
IconButton(onClick = state.onSearchButtonClick) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_search_24),
|
||||
contentDescription = "Search",
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
}
|
||||
|
||||
if (state is Title.Manage) {
|
||||
IconButton(onClick = state.onAddCustomTokenClick) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_plus_24),
|
||||
contentDescription = "Add custom token",
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun InputContent(state: InputField, modifier: Modifier = Modifier) {
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
|
||||
BasicTextField(
|
||||
value = state.value,
|
||||
onValueChange = state.onValueChange,
|
||||
modifier = modifier.focusRequester(focusRequester),
|
||||
textStyle = TangemTheme.typography.subtitle1.copy(
|
||||
fontWeight = FontWeight.Normal,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
),
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Text, imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(onSearch = { keyboardController?.hide() }),
|
||||
singleLine = true,
|
||||
maxLines = 1,
|
||||
cursorBrush = SolidColor(value = TangemTheme.colors.stroke.secondary),
|
||||
) { innerTextField ->
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing28,
|
||||
top = TangemTheme.dimens.spacing2,
|
||||
end = TangemTheme.dimens.spacing16,
|
||||
),
|
||||
) {
|
||||
Hint(value = state.value)
|
||||
innerTextField()
|
||||
}
|
||||
|
||||
IconButton(onClick = state.onCleanButtonClick) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_cross_rounded_24),
|
||||
contentDescription = "Clean entered value",
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is a hack because Compose can't show the keyboard if the input field is not found.
|
||||
// So first I request focus and make a delay and then show the keyboard.
|
||||
val keyboardManager = LocalSoftwareKeyboardController.current
|
||||
LaunchedEffect(keyboardManager) {
|
||||
focusRequester.requestFocus()
|
||||
delay(timeMillis = 100)
|
||||
keyboardManager?.show()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Hint(value: String) {
|
||||
if (value.isBlank()) {
|
||||
Row(
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens.spacing4),
|
||||
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing4),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_search_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.secondary,
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.common_search),
|
||||
modifier = Modifier.padding(bottom = TangemTheme.dimens.spacing2),
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
maxLines = 1,
|
||||
// FIXME("Incorrect typography. Replace with typography from design system")
|
||||
style = TangemTheme.typography.subtitle1.copy(fontSize = 18.sp, fontWeight = FontWeight.Normal),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddTokensToolbar_EditAccess() {
|
||||
TangemThemePreview {
|
||||
TokensListToolbar(
|
||||
state = Title.Manage(
|
||||
titleResId = R.string.main_manage_tokens,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
onAddCustomTokenClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddTokensToolbar_ReadAccess() {
|
||||
TangemThemePreview {
|
||||
TokensListToolbar(
|
||||
state = Title.Read(
|
||||
titleResId = R.string.common_search_tokens,
|
||||
onBackButtonClick = {},
|
||||
onSearchButtonClick = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun Preview_AddTokensToolbar_SearchInputField() {
|
||||
var value by remember { mutableStateOf("") }
|
||||
|
||||
TangemThemePreview {
|
||||
TokensListToolbar(
|
||||
state = InputField(
|
||||
onBackButtonClick = {},
|
||||
value = value,
|
||||
onValueChange = { value = it },
|
||||
onCleanButtonClick = { value = "" },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.viewmodels
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.blockchain.common.Token
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.ManageTokens
|
||||
|
||||
/** Analytics sender for tokens list screen */
|
||||
class TokensListAnalyticsSender(private val analyticsEventHandler: AnalyticsEventHandler) {
|
||||
|
||||
fun sendWhenScreenOpened() {
|
||||
analyticsEventHandler.send(event = ManageTokens.ScreenOpened())
|
||||
}
|
||||
|
||||
fun sendWhenTokenAdded(token: Token) {
|
||||
analyticsEventHandler.send(
|
||||
event = ManageTokens.TokenSwitcherChanged(
|
||||
type = AnalyticsParam.CurrencyType.Token(token),
|
||||
state = AnalyticsParam.OnOffState.On,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendWhenBlockchainAdded(blockchain: Blockchain) {
|
||||
analyticsEventHandler.send(
|
||||
event = ManageTokens.TokenSwitcherChanged(
|
||||
type = AnalyticsParam.CurrencyType.Blockchain(blockchain),
|
||||
state = AnalyticsParam.OnOffState.On,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendWhenTokenRemoved(token: Token) {
|
||||
analyticsEventHandler.send(
|
||||
event = ManageTokens.TokenSwitcherChanged(
|
||||
type = AnalyticsParam.CurrencyType.Token(token),
|
||||
state = AnalyticsParam.OnOffState.Off,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendWhenBlockchainRemoved(blockchain: Blockchain) {
|
||||
analyticsEventHandler.send(
|
||||
event = ManageTokens.TokenSwitcherChanged(
|
||||
type = AnalyticsParam.CurrencyType.Blockchain(blockchain),
|
||||
state = AnalyticsParam.OnOffState.Off,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fun sendWhenSaveButtonClicked() {
|
||||
analyticsEventHandler.send(ManageTokens.ButtonSaveChanges())
|
||||
}
|
||||
|
||||
fun sendWhenTokenSearched() {
|
||||
analyticsEventHandler.send(ManageTokens.TokenSearched())
|
||||
}
|
||||
|
||||
fun sendWhenAddCustomTokenClicked() {
|
||||
analyticsEventHandler.send(ManageTokens.ButtonCustomToken())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
package com.tangem.tap.features.tokens.impl.presentation.viewmodels
|
||||
|
||||
import com.tangem.blockchain.common.Blockchain
|
||||
import com.tangem.domain.tokens.TokenWithBlockchain
|
||||
|
||||
internal data class TokensListCryptoCurrencies(
|
||||
val coins: List<Blockchain>,
|
||||
val tokens: List<TokenWithBlockchain>,
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue