Updated on 2026-08-14
This commit is contained in:
commit
3fc0a75d40
388 changed files with 17559 additions and 3009 deletions
|
|
@ -51,6 +51,7 @@ dependencies {
|
|||
implementation(projects.domain.balanceHiding)
|
||||
implementation(projects.domain.balanceHiding.models)
|
||||
implementation(projects.domain.transaction)
|
||||
implementation(projects.domain.analytics)
|
||||
|
||||
implementation(projects.common)
|
||||
implementation(projects.core.analytics)
|
||||
|
|
@ -74,6 +75,7 @@ dependencies {
|
|||
implementation(projects.data.tokens)
|
||||
implementation(projects.data.txhistory)
|
||||
implementation(projects.data.wallets)
|
||||
implementation(projects.data.analytics)
|
||||
|
||||
/** Features */
|
||||
implementation(projects.features.onboarding)
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import com.tangem.blockchain.common.BlockchainSdkConfig
|
|||
import com.tangem.blockchain.common.WalletManagerFactory
|
||||
import com.tangem.blockchain.network.BlockchainSdkRetrofitBuilder
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||
import com.tangem.core.featuretoggle.manager.FeatureTogglesManager
|
||||
import com.tangem.data.source.preferences.PreferencesDataSource
|
||||
import com.tangem.datasource.api.common.MoshiConverter
|
||||
|
|
@ -216,6 +217,9 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
|
||||
@Inject
|
||||
lateinit var sendFeatureToggles: SendFeatureToggles
|
||||
|
||||
@Inject
|
||||
lateinit var oneTimeEventFilter: OneTimeEventFilter
|
||||
// endregion Injected
|
||||
|
||||
override fun onCreate() {
|
||||
|
|
@ -353,6 +357,8 @@ internal class TapApplication : Application(), ImageLoaderFactory {
|
|||
factory.addHandlerBuilder(AppsFlyerAnalyticsHandler.Builder())
|
||||
factory.addHandlerBuilder(FirebaseAnalyticsHandler.Builder())
|
||||
|
||||
factory.addFilter(oneTimeEventFilter)
|
||||
|
||||
val buildData = AnalyticsHandlerBuilder.Data(
|
||||
application = application,
|
||||
config = config,
|
||||
|
|
|
|||
|
|
@ -65,15 +65,16 @@ class DialogManager : StoreSubscriber<GlobalState> {
|
|||
messageRes = R.string.wallet_connect_scanner_error_not_valid_card,
|
||||
context = context,
|
||||
)
|
||||
is WalletConnectDialog.AddNetwork ->
|
||||
is WalletConnectDialog.AddNetwork -> {
|
||||
val message = context.getString(
|
||||
R.string.wallet_connect_error_missing_blockchains,
|
||||
) + state.dialog.networks.joinToString()
|
||||
SimpleAlertDialog.create(
|
||||
titleRes = R.string.wallet_connect_title,
|
||||
message = context.getString(
|
||||
R.string.wallet_connect_network_not_found_format,
|
||||
state.dialog.network,
|
||||
),
|
||||
message = message,
|
||||
context = context,
|
||||
)
|
||||
}
|
||||
is WalletConnectDialog.OpeningSessionRejected -> {
|
||||
SimpleAlertDialog.create(
|
||||
titleRes = R.string.wallet_connect_title,
|
||||
|
|
|
|||
|
|
@ -2,16 +2,18 @@ package com.tangem.tap.common.analytics.converters
|
|||
|
||||
import com.tangem.common.Converter
|
||||
import com.tangem.domain.common.CardTypesResolver
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
*/
|
||||
class TopUpEventConverter : Converter<CardTypesResolver, Basic.ToppedUp?> {
|
||||
class TopUpEventConverter : Converter<Pair<UserWalletId, CardTypesResolver>, Basic.ToppedUp?> {
|
||||
|
||||
override fun convert(value: CardTypesResolver): Basic.ToppedUp? {
|
||||
val paramCardCurrency = ParamCardCurrencyConverter().convert(value) ?: return null
|
||||
override fun convert(value: Pair<UserWalletId, CardTypesResolver>): Basic.ToppedUp? {
|
||||
val (userWalletId, resolver) = value
|
||||
val paramCardCurrency = ParamCardCurrencyConverter().convert(resolver) ?: return null
|
||||
|
||||
return Basic.ToppedUp(paramCardCurrency)
|
||||
return Basic.ToppedUp(userWalletId, paramCardCurrency)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.tap.common.analytics.events
|
||||
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.tap.features.details.redux.SecurityOption
|
||||
|
||||
sealed class AnalyticsParam {
|
||||
|
|
@ -14,7 +15,7 @@ sealed class AnalyticsParam {
|
|||
|
||||
// MultiCurrency or CurrencyType
|
||||
sealed class CardCurrency(val value: String) {
|
||||
object MultiCurrency : CardCurrency("Multicurrency")
|
||||
object MultiCurrency : CardCurrency(value = "Multicurrency")
|
||||
class SingleCurrency(type: CurrencyType) : CardCurrency(type.value)
|
||||
}
|
||||
|
||||
|
|
@ -139,6 +140,22 @@ sealed class AnalyticsParam {
|
|||
object SeedImport : WalletCreationType(value = "Seed Import")
|
||||
}
|
||||
|
||||
sealed class AppTheme(val value: String) {
|
||||
object System : AppTheme("System")
|
||||
object Dark : AppTheme("Dark")
|
||||
object Light : AppTheme("Light")
|
||||
|
||||
companion object {
|
||||
fun fromAppThemeMode(mode: AppThemeMode): AppTheme {
|
||||
return when (mode) {
|
||||
AppThemeMode.FORCE_DARK -> Dark
|
||||
AppThemeMode.FORCE_LIGHT -> Light
|
||||
AppThemeMode.FOLLOW_SYSTEM -> System
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object Key {
|
||||
const val BLOCKCHAIN = "blockchain"
|
||||
const val TOKEN = "Token"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package com.tangem.tap.common.analytics.events
|
||||
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
/**
|
||||
[REDACTED_AUTHOR]
|
||||
|
|
@ -50,10 +52,15 @@ sealed class Basic(
|
|||
}
|
||||
}
|
||||
|
||||
class ToppedUp(currency: AnalyticsParam.CardCurrency) : Basic(
|
||||
event = "Topped up",
|
||||
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
|
||||
)
|
||||
class ToppedUp(userWalletId: UserWalletId, currency: AnalyticsParam.CardCurrency) :
|
||||
Basic(
|
||||
event = "Topped up",
|
||||
params = mapOf(AnalyticsParam.CURRENCY to currency.value),
|
||||
),
|
||||
OneTimeAnalyticsEvent {
|
||||
|
||||
override val oneTimeEventId: String = id + userWalletId.stringValue
|
||||
}
|
||||
|
||||
class TransactionSent(sentFrom: AnalyticsParam.TxSentFrom, memoType: MemoType) : Basic(
|
||||
event = "Transaction sent",
|
||||
|
|
|
|||
|
|
@ -84,5 +84,10 @@ sealed class Settings(
|
|||
event = "Main Currency Changed",
|
||||
params = mapOf("Currency Type" to currencyType),
|
||||
)
|
||||
|
||||
class ThemeSwitched(theme: AnalyticsParam.AppTheme) : AppSettings(
|
||||
event = "App Theme Switched",
|
||||
params = mapOf("State" to theme.value),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -9,8 +9,8 @@ class AmplitudeAnalyticsHandler(
|
|||
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun send(event: String, params: Map<String, String>) {
|
||||
client.logEvent(event, params)
|
||||
override fun send(eventId: String, params: Map<String, String>) {
|
||||
client.logEvent(eventId, params)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ class AppsFlyerAnalyticsHandler(
|
|||
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun send(event: String, params: Map<String, String>) {
|
||||
client.logEvent(event, params)
|
||||
override fun send(eventId: String, params: Map<String, String>) {
|
||||
client.logEvent(eventId, params)
|
||||
}
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
|
|
|
|||
|
|
@ -14,8 +14,8 @@ class FirebaseAnalyticsHandler(
|
|||
|
||||
override fun id(): String = ID
|
||||
|
||||
override fun send(event: String, params: Map<String, String>) {
|
||||
client.logEvent(event, params)
|
||||
override fun send(eventId: String, params: Map<String, String>) {
|
||||
client.logEvent(eventId, params)
|
||||
}
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ class TopUpController(
|
|||
|
||||
if (cardBalanceState.isToppedUp()) {
|
||||
topupWalletStorage.save(topupInfo.copy(cardBalanceState = DataSourceTopupInfo.CardBalanceState.Full))
|
||||
TopUpEventConverter().convert(cardTypesResolver)?.let {
|
||||
TopUpEventConverter().convert(value = userWalletId to cardTypesResolver)?.let {
|
||||
Analytics.send(it)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.analytics.CheckIsWalletToppedUpUseCase
|
||||
import com.tangem.domain.analytics.repository.AnalyticsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.components.ViewModelComponent
|
||||
|
||||
@Module
|
||||
@InstallIn(ViewModelComponent::class)
|
||||
internal object AnalyticsDomainModule {
|
||||
|
||||
@Provides
|
||||
fun provideCheckIsWalletToppedUpUseCase(analyticsRepository: AnalyticsRepository): CheckIsWalletToppedUpUseCase {
|
||||
return CheckIsWalletToppedUpUseCase(analyticsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -91,4 +91,16 @@ internal object SettingsDomainModule {
|
|||
): UpdateBalanceHidingSettingsUseCase {
|
||||
return UpdateBalanceHidingSettingsUseCase(balanceHidingRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSetWalletsScrollPreviewIsShown(settingsRepository: SettingsRepository): NeverToShowWalletsScrollPreview {
|
||||
return NeverToShowWalletsScrollPreview(settingsRepository = settingsRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideIsWalletsScrollPreviewEnabled(settingsRepository: SettingsRepository): IsWalletsScrollPreviewEnabled {
|
||||
return IsWalletsScrollPreviewEnabled(settingsRepository = settingsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -2,10 +2,7 @@ package com.tangem.tap.di.domain
|
|||
|
||||
import com.tangem.domain.exchange.RampStateManager
|
||||
import com.tangem.domain.tokens.*
|
||||
import com.tangem.domain.tokens.repository.CurrenciesRepository
|
||||
import com.tangem.domain.tokens.repository.MarketCryptoCurrencyRepository
|
||||
import com.tangem.domain.tokens.repository.NetworksRepository
|
||||
import com.tangem.domain.tokens.repository.QuotesRepository
|
||||
import com.tangem.domain.tokens.repository.*
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -231,4 +228,10 @@ internal object TokensDomainModule {
|
|||
): GetMissedAddressesCryptoCurrenciesUseCase {
|
||||
return GetMissedAddressesCryptoCurrenciesUseCase(currenciesRepository = currenciesRepository)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetGlobalTokenListUseCase(tokensListRepository: TokensListRepository): GetGlobalTokenListUseCase {
|
||||
return GetGlobalTokenListUseCase(repository = tokensListRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
package com.tangem.tap.di.domain
|
||||
|
||||
import com.tangem.domain.card.repository.CardSdkConfigRepository
|
||||
import com.tangem.domain.demo.IsDemoCardUseCase
|
||||
import com.tangem.domain.transaction.usecase.GetFeeUseCase
|
||||
import com.tangem.domain.transaction.usecase.SendTransactionUseCase
|
||||
import com.tangem.domain.walletmanager.WalletManagersFacade
|
||||
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
|
||||
import dagger.Module
|
||||
|
|
@ -15,10 +18,24 @@ internal object TransactionDomainModule {
|
|||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideGetUseCase(
|
||||
fun provideGetFeeUseCase(
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
dispatchers: CoroutineDispatcherProvider,
|
||||
): GetFeeUseCase {
|
||||
return GetFeeUseCase(walletManagersFacade, dispatchers)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@ViewModelScoped
|
||||
fun provideSendTransactionUseCase(
|
||||
isDemoCardUseCase: IsDemoCardUseCase,
|
||||
walletManagersFacade: WalletManagersFacade,
|
||||
cardSdkConfigRepository: CardSdkConfigRepository,
|
||||
): SendTransactionUseCase {
|
||||
return SendTransactionUseCase(
|
||||
isDemoCardUseCase = isDemoCardUseCase,
|
||||
cardSdkConfigRepository = cardSdkConfigRepository,
|
||||
walletManagersFacade = walletManagersFacade,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -67,15 +67,17 @@ internal class BiometricUserWalletsListManager(
|
|||
}
|
||||
}
|
||||
.map {
|
||||
if (throwIfNotAllWalletsUnlocked && state.value.userWallets.any(UserWallet::isLocked)) {
|
||||
Timber.e("Not all user wallets have been unlocked")
|
||||
val userWallets = state.value.userWallets
|
||||
|
||||
if (throwIfNotAllWalletsUnlocked && userWallets.any(UserWallet::isLocked)) {
|
||||
Timber.e("Some user wallets remain locked")
|
||||
throw UserWalletsListError.NotAllUserWalletsUnlocked
|
||||
}
|
||||
|
||||
val selectedUserWallet = selectedUserWalletSync
|
||||
if (selectedUserWallet == null || selectedUserWallet.isLocked) {
|
||||
Timber.e("Unable to find selected user wallet")
|
||||
throw UserWalletsListError.NoUserWalletSelected
|
||||
findAndSetUnlockedUserWallet(userWallets)
|
||||
?: throw UserWalletsListError.NoUserWalletSelected
|
||||
} else {
|
||||
selectedUserWallet
|
||||
}
|
||||
|
|
@ -280,13 +282,14 @@ internal class BiometricUserWalletsListManager(
|
|||
prevSelectedWalletId: UserWalletId?,
|
||||
userWallets: List<UserWallet>,
|
||||
): UserWalletId? {
|
||||
val findUnlockedAndSet = {
|
||||
userWallets.firstOrNull { !it.isLocked }
|
||||
?.walletId
|
||||
?.also { selectedUserWalletRepository.set(it) }
|
||||
}
|
||||
return prevSelectedWalletId
|
||||
?: (selectedUserWalletRepository.get() ?: findAndSetUnlockedUserWallet(userWallets)?.walletId)
|
||||
}
|
||||
|
||||
return prevSelectedWalletId ?: (selectedUserWalletRepository.get() ?: findUnlockedAndSet())
|
||||
private fun findAndSetUnlockedUserWallet(userWallets: List<UserWallet>): UserWallet? {
|
||||
return userWallets
|
||||
.firstOrNull { !it.isLocked }
|
||||
?.also { selectedUserWalletRepository.set(it.walletId) }
|
||||
}
|
||||
|
||||
private fun changeSelectedUserWalletIdIfNeeded(walletsIdsToRemove: List<UserWalletId>) {
|
||||
|
|
|
|||
|
|
@ -75,9 +75,10 @@ interface FragmentOnBackPressedHandler {
|
|||
|
||||
@SuppressLint("FragmentBackPressedCallback")
|
||||
fun Fragment.addBackPressHandler(handler: FragmentOnBackPressedHandler) {
|
||||
requireActivity().onBackPressedDispatcher.addCallback {
|
||||
handler.handleOnBackPressed()
|
||||
}
|
||||
requireActivity().onBackPressedDispatcher.addCallback(
|
||||
owner = this,
|
||||
onBackPressed = { handler.handleOnBackPressed() },
|
||||
)
|
||||
|
||||
view?.findViewById<Toolbar>(R.id.toolbar)?.setNavigationOnClickListener {
|
||||
handler.handleOnBackPressed()
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ class WalletConnectMiddleware {
|
|||
).guard {
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.AddNetwork(blockchain.fullName),
|
||||
WalletConnectDialog.AddNetwork(listOf(blockchain.fullName)),
|
||||
),
|
||||
)
|
||||
return@launch
|
||||
|
|
@ -325,7 +325,7 @@ class WalletConnectMiddleware {
|
|||
is WalletConnectError.ApprovalErrorAddNetwork -> {
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.UnsupportedNetwork(action.error.networks),
|
||||
WalletConnectDialog.AddNetwork(action.error.networks),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -417,7 +417,7 @@ class WalletConnectMiddleware {
|
|||
store.dispatchOnMain(WalletConnectAction.FailureEstablishingSession(session.session))
|
||||
store.dispatchOnMain(
|
||||
GlobalAction.ShowDialog(
|
||||
WalletConnectDialog.AddNetwork(blockchain.fullName),
|
||||
WalletConnectDialog.AddNetwork(listOf(blockchain.fullName)),
|
||||
),
|
||||
)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ sealed class WalletConnectDialog : StateDialog {
|
|||
data class ClipboardOrScanQr(val clipboardUri: String) : WalletConnectDialog()
|
||||
object UnsupportedCard : WalletConnectDialog()
|
||||
data class UnsupportedNetwork(val networks: List<String>? = null) : WalletConnectDialog()
|
||||
data class AddNetwork(val network: String) : WalletConnectDialog()
|
||||
data class AddNetwork(val networks: List<String>) : WalletConnectDialog()
|
||||
object OpeningSessionRejected : WalletConnectDialog()
|
||||
object SessionTimeout : WalletConnectDialog()
|
||||
data class ApproveWcSession(
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.LifecycleCoroutineScope
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.appcurrency.repository.AppCurrencyRepository
|
||||
import com.tangem.domain.apptheme.model.AppThemeMode
|
||||
import com.tangem.tap.common.analytics.events.AnalyticsParam
|
||||
import com.tangem.tap.common.analytics.events.Settings
|
||||
import com.tangem.tap.common.entities.FiatCurrency
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.dispatchWithMain
|
||||
|
|
@ -121,6 +124,11 @@ internal class AppSettingsViewModel(
|
|||
dialog = dialogsFactory.createThemeModeSelectorDialog(
|
||||
selectedModeIndex = selectedMode.ordinal,
|
||||
onSelect = { mode ->
|
||||
Analytics.send(
|
||||
event = Settings.AppSettings.ThemeSwitched(
|
||||
theme = AnalyticsParam.AppTheme.fromAppThemeMode(mode),
|
||||
),
|
||||
)
|
||||
store.dispatchOnMain(DetailsAction.AppSettings.ChangeAppThemeMode(mode))
|
||||
dismissDialog()
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import com.tangem.core.ui.components.SpacerH4
|
||||
import com.tangem.core.ui.components.SpacerW32
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsItemsFactory
|
||||
import com.tangem.tap.features.details.ui.appsettings.AppSettingsScreenState.Item
|
||||
import com.tangem.core.ui.components.TangemSwitch
|
||||
|
||||
@Composable
|
||||
internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier) {
|
||||
|
|
@ -63,6 +63,7 @@ internal fun SettingsSwitchItem(item: Item.Switch, modifier: Modifier = Modifier
|
|||
checked = item.isChecked,
|
||||
enabled = item.isEnabled,
|
||||
onCheckedChange = item.onCheckedChange,
|
||||
checkedColor = TangemTheme.colors.icon.accent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,10 @@ import com.tangem.domain.common.util.twinsIsTwinned
|
|||
import com.tangem.domain.models.scan.ProductType
|
||||
import com.tangem.domain.models.scan.ScanResponse
|
||||
import com.tangem.domain.userwallets.UserWalletBuilder
|
||||
import com.tangem.domain.userwallets.UserWalletIdBuilder
|
||||
import com.tangem.tap.*
|
||||
import com.tangem.tap.common.analytics.converters.ParamCardCurrencyConverter
|
||||
import com.tangem.tap.common.analytics.events.Basic
|
||||
import com.tangem.tap.common.extensions.dispatchOnMain
|
||||
import com.tangem.tap.common.extensions.onUserWalletSelected
|
||||
import com.tangem.tap.common.extensions.removeContext
|
||||
|
|
@ -117,6 +120,15 @@ object OnboardingHelper {
|
|||
Analytics.removeContext()
|
||||
}
|
||||
|
||||
fun sendToppedUpEvent(scanResponse: ScanResponse) {
|
||||
val userWalletId = UserWalletIdBuilder.scanResponse(scanResponse).build()
|
||||
val currency = ParamCardCurrencyConverter().convert(scanResponse.cardTypesResolver)
|
||||
|
||||
if (userWalletId != null && currency != null) {
|
||||
Analytics.send(Basic.ToppedUp(userWalletId, currency))
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun proceedWithScanResponse(scanResponse: ScanResponse, backupCardsIds: List<String>?) {
|
||||
val userWallet = UserWalletBuilder(scanResponse)
|
||||
.backupCardsIds(backupCardsIds?.toSet())
|
||||
|
|
|
|||
|
|
@ -152,6 +152,8 @@ private fun handleNoteAction(appState: () -> AppState?, action: Action, dispatch
|
|||
}
|
||||
is OnboardingNoteAction.Balance.Set -> {
|
||||
if (action.balance.balanceIsToppedUp()) {
|
||||
OnboardingHelper.sendToppedUpEvent(scanResponse)
|
||||
|
||||
store.state.globalState.topUpController?.send(scanResponse, AnalyticsParam.CardBalanceState.Full)
|
||||
store.dispatch(OnboardingNoteAction.SetStepOfScreen(OnboardingNoteStep.Done))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -260,6 +260,8 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
|
|||
}
|
||||
is TwinCardsAction.Balance.Set -> {
|
||||
if (action.balance.balanceIsToppedUp()) {
|
||||
OnboardingHelper.sendToppedUpEvent(getScanResponse())
|
||||
|
||||
store.state.globalState.topUpController?.send(getScanResponse(), AnalyticsParam.CardBalanceState.Full)
|
||||
store.dispatchOnMain(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ import com.tangem.domain.tokens.AddCryptoCurrenciesUseCase
|
|||
import com.tangem.domain.tokens.TokenWithBlockchain
|
||||
import com.tangem.domain.tokens.TokensAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.walletconnect.WalletConnectActions
|
||||
import com.tangem.domain.wallets.models.UserWallet
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
import com.tangem.operations.derivation.ExtendedPublicKeysMap
|
||||
import com.tangem.tap.*
|
||||
|
|
@ -90,13 +92,17 @@ object TokensMiddleware {
|
|||
if (scanResponse.supportsHdWallet()) {
|
||||
deriveMissingCoins(scanResponse = scanResponse, currencyList = currencyList) {
|
||||
submitNewAdd(
|
||||
userWalletId = action.userWallet.walletId,
|
||||
userWallet = action.userWallet,
|
||||
updatedScanResponse = it,
|
||||
currencyList = currencyList,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
submitNewAdd(userWalletId = action.userWallet.walletId, scanResponse, currencyList = currencyList)
|
||||
submitNewAdd(
|
||||
userWallet = action.userWallet,
|
||||
updatedScanResponse = scanResponse,
|
||||
currencyList = currencyList,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -375,16 +381,18 @@ object TokensMiddleware {
|
|||
}
|
||||
|
||||
private fun submitNewAdd(
|
||||
userWalletId: UserWalletId,
|
||||
userWallet: UserWallet,
|
||||
updatedScanResponse: ScanResponse,
|
||||
currencyList: List<CryptoCurrency>,
|
||||
) {
|
||||
scope.launch {
|
||||
userWalletsListManager.update(
|
||||
userWalletId = userWalletId,
|
||||
userWalletId = userWallet.walletId,
|
||||
update = { it.copy(scanResponse = updatedScanResponse) },
|
||||
).doOnSuccess {
|
||||
addCryptoCurrenciesUseCase(userWalletId, currencyList)
|
||||
addCryptoCurrenciesUseCase(userWallet.walletId, currencyList).onRight {
|
||||
store.dispatch(action = WalletConnectActions.New.SetupUserChains(userWallet = userWallet))
|
||||
}
|
||||
}
|
||||
}
|
||||
store.dispatchOnMain(NavigationAction.PopBackTo())
|
||||
|
|
|
|||
|
|
@ -9,11 +9,8 @@ import com.tangem.common.extensions.guard
|
|||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.navigation.AppScreen
|
||||
import com.tangem.core.navigation.NavigationAction
|
||||
import com.tangem.domain.common.extensions.toCoinId
|
||||
import com.tangem.domain.common.extensions.toNetworkId
|
||||
import com.tangem.domain.tokens.legacy.TradeCryptoAction
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import com.tangem.domain.tokens.model.Network
|
||||
import com.tangem.domain.tokens.model.NetworkAddress
|
||||
import com.tangem.feature.swap.presentation.SwapFragment
|
||||
import com.tangem.features.send.api.navigation.SendRouter
|
||||
|
|
@ -26,7 +23,6 @@ import com.tangem.tap.common.extensions.dispatchOnMain
|
|||
import com.tangem.tap.common.extensions.dispatchOpenUrl
|
||||
import com.tangem.tap.common.redux.AppState
|
||||
import com.tangem.tap.domain.TapError
|
||||
import com.tangem.tap.domain.tokens.getIconUrl
|
||||
import com.tangem.tap.features.demo.DemoHelper
|
||||
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
|
||||
import com.tangem.tap.features.send.redux.PrepareSendScreen
|
||||
|
|
@ -41,9 +37,6 @@ import com.tangem.tap.proxy.redux.DaggerGraphState
|
|||
import com.tangem.tap.scope
|
||||
import com.tangem.tap.store
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.encodeToString
|
||||
import kotlinx.serialization.json.Json
|
||||
import com.tangem.feature.swap.domain.models.domain.Currency as SwapCurrency
|
||||
|
||||
@Suppress("LargeClass")
|
||||
class TradeCryptoMiddleware {
|
||||
|
|
@ -58,17 +51,12 @@ class TradeCryptoMiddleware {
|
|||
is TradeCryptoAction.SendCrypto -> preconfigureAndOpenSendScreen(action)
|
||||
is TradeCryptoAction.FinishSelling -> openReceiptUrl(action.transactionId)
|
||||
is TradeCryptoAction.Swap -> {
|
||||
openSwap(
|
||||
currency = store.state.walletState.selectedWalletData?.currency?.toSwapCurrency(),
|
||||
derivationPath = store.state.walletState.selectedWalletData?.currency?.derivationPath,
|
||||
)
|
||||
// todo remove old flow
|
||||
}
|
||||
is TradeCryptoAction.New.Buy -> proceedNewBuyAction(state, action)
|
||||
is TradeCryptoAction.New.Sell -> proceedNewSellAction(action)
|
||||
is TradeCryptoAction.New.Swap -> openSwap(
|
||||
currency = action.cryptoCurrency.toSwapCurrency(),
|
||||
derivationPath = action.cryptoCurrency.network.derivationPath.value,
|
||||
network = action.cryptoCurrency.network,
|
||||
currency = action.cryptoCurrency,
|
||||
)
|
||||
is TradeCryptoAction.New.SendToken -> handleNewSendToken(action = action)
|
||||
is TradeCryptoAction.New.SendCoin -> handleNewSendCoin(action = action)
|
||||
|
|
@ -274,70 +262,14 @@ class TradeCryptoMiddleware {
|
|||
)?.let { store.dispatchOpenUrl(it) }
|
||||
}
|
||||
|
||||
private fun openSwap(currency: SwapCurrency?, derivationPath: String?, network: Network? = null) {
|
||||
private fun openSwap(currency: CryptoCurrency) {
|
||||
val bundle = bundleOf(
|
||||
SwapFragment.CURRENCY_BUNDLE_KEY to Json.encodeToString(currency),
|
||||
SwapFragment.DERIVATION_PATH to derivationPath,
|
||||
SwapFragment.NETWORK to network,
|
||||
SwapFragment.CURRENCY_BUNDLE_KEY to currency,
|
||||
)
|
||||
|
||||
store.dispatchOnMain(NavigationAction.NavigateTo(screen = AppScreen.Swap, bundle = bundle))
|
||||
}
|
||||
|
||||
private fun CryptoCurrency.toSwapCurrency(): SwapCurrency {
|
||||
val blockchain = Blockchain.fromId(network.id.value)
|
||||
|
||||
return when (this) {
|
||||
is CryptoCurrency.Coin -> {
|
||||
SwapCurrency.NativeToken(
|
||||
id = blockchain.toCoinId(),
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
networkId = blockchain.toNetworkId(),
|
||||
// no need to set logoUrl for blockchain cause
|
||||
// error when form url with coinId, coinId of eth and arbitrum the same
|
||||
logoUrl = "",
|
||||
)
|
||||
}
|
||||
is CryptoCurrency.Token -> {
|
||||
SwapCurrency.NonNativeToken(
|
||||
id = id.rawCurrencyId ?: "",
|
||||
name = name,
|
||||
symbol = symbol,
|
||||
networkId = blockchain.toNetworkId(),
|
||||
logoUrl = getIconUrl(id.rawCurrencyId ?: ""),
|
||||
contractAddress = contractAddress,
|
||||
decimalCount = decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Currency.toSwapCurrency(): SwapCurrency {
|
||||
return when (this) {
|
||||
is Currency.Blockchain -> {
|
||||
SwapCurrency.NativeToken(
|
||||
id = blockchain.toCoinId(),
|
||||
name = this.currencyName,
|
||||
symbol = this.currencySymbol,
|
||||
networkId = this.blockchain.toNetworkId(),
|
||||
// no need to set logoUrl for blockchain cause
|
||||
// error when form url with coinId, coinId of eth and arbitrum the same
|
||||
logoUrl = "",
|
||||
)
|
||||
}
|
||||
is Currency.Token -> SwapCurrency.NonNativeToken(
|
||||
id = this.token.id ?: "",
|
||||
name = this.currencyName,
|
||||
symbol = this.currencySymbol,
|
||||
networkId = this.blockchain.toNetworkId(),
|
||||
logoUrl = getIconUrl(this.token.id ?: ""),
|
||||
contractAddress = this.token.contractAddress,
|
||||
decimalCount = decimals,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewSendToken(action: TradeCryptoAction.New.SendToken) {
|
||||
val currency = action.tokenCurrency
|
||||
val blockchain = Blockchain.fromId(currency.network.id.value)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package com.tangem.tap.network.auth
|
||||
|
||||
import com.tangem.datasource.config.ConfigManager
|
||||
import com.tangem.datasource.local.userwallet.UserWalletsStore
|
||||
import com.tangem.lib.auth.ExpressAuthProvider
|
||||
import com.tangem.lib.auth.sessionId.ExpressSessionIdGenerator
|
||||
import java.util.UUID
|
||||
|
||||
class ExpressAuthProviderImpl(
|
||||
private val userWalletsStore: UserWalletsStore,
|
||||
private val configManager: ConfigManager,
|
||||
) : ExpressAuthProvider, ExpressSessionIdGenerator {
|
||||
|
||||
private var uuid = UUID.randomUUID()
|
||||
|
||||
override fun getApiKey(): String {
|
||||
return configManager.config.tangemExpressApiKey
|
||||
}
|
||||
|
||||
override fun getUserId(): String {
|
||||
return userWalletsStore.selectedUserWalletOrNull?.walletId?.stringValue ?: ""
|
||||
}
|
||||
|
||||
override fun getSessionId(): String {
|
||||
return uuid.toString()
|
||||
}
|
||||
|
||||
override fun generateNewSessionId() {
|
||||
uuid = UUID.randomUUID()
|
||||
}
|
||||
}
|
||||
|
|
@ -224,20 +224,18 @@ class TransactionManagerImpl(
|
|||
// for not EVM blockchains set gasLimit ZERO for now
|
||||
when (fee.data) {
|
||||
is TransactionFee.Single -> {
|
||||
val fee = (fee.data as TransactionFee.Single).normal
|
||||
val normalFee = (fee.data as TransactionFee.Single).normal
|
||||
val singleFee = ProxyFee(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = fee.amount),
|
||||
fee = convertToProxyAmount(amount = normalFee.amount),
|
||||
)
|
||||
ProxyFees(
|
||||
minFee = singleFee,
|
||||
normalFee = singleFee,
|
||||
priorityFee = singleFee,
|
||||
ProxyFees.SingleFee(
|
||||
singleFee = singleFee,
|
||||
)
|
||||
}
|
||||
is TransactionFee.Choosable -> {
|
||||
val choosableFee = fee.data as TransactionFee.Choosable
|
||||
ProxyFees(
|
||||
ProxyFees.MultipleFees(
|
||||
minFee = ProxyFee(
|
||||
gasLimit = BigInteger.ZERO,
|
||||
fee = convertToProxyAmount(amount = choosableFee.minimum.amount),
|
||||
|
|
@ -280,7 +278,7 @@ class TransactionManagerImpl(
|
|||
).increaseBigIntegerByPercents(increaseBy)
|
||||
return when (val gasPrice = walletManager.getGasPrice()) {
|
||||
is Result.Success -> {
|
||||
createProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
|
||||
createMultipleProxyFees(gasPrice = gasPrice.data, gasLimit = gasLimit, blockchain = blockchain)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
error(gasPrice.error.message ?: gasPrice.error.customMessage)
|
||||
|
|
@ -316,7 +314,7 @@ class TransactionManagerImpl(
|
|||
fee = convertToProxyAmount(amount = choosableFee.priority.amount),
|
||||
)
|
||||
|
||||
ProxyFees(
|
||||
ProxyFees.MultipleFees(
|
||||
minFee = minProxyFee,
|
||||
normalFee = normalProxyFee,
|
||||
priorityFee = priorityProxyFee,
|
||||
|
|
@ -456,7 +454,7 @@ class TransactionManagerImpl(
|
|||
* @param gasLimit
|
||||
* @param blockchain
|
||||
*/
|
||||
private fun createProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees {
|
||||
private fun createMultipleProxyFees(gasPrice: BigInteger, gasLimit: BigInteger, blockchain: Blockchain): ProxyFees {
|
||||
val gasPriceNormal = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_NORMAL_FEE)
|
||||
val gasPricePriority = gasPrice.increaseBigIntegerByPercents(MULTIPLIER_GAS_PRICE_FOR_PRIORITY_FEE)
|
||||
val feeMin = gasLimit.multiply(gasPrice).toBigDecimal(
|
||||
|
|
@ -495,7 +493,7 @@ class TransactionManagerImpl(
|
|||
decimals = blockchain.decimals(),
|
||||
),
|
||||
)
|
||||
return ProxyFees(
|
||||
return ProxyFees.MultipleFees(
|
||||
minFee = minFee,
|
||||
normalFee = normalFee,
|
||||
priorityFee = priorityFee,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/send_fee_label"
|
||||
android:text="@string/common_fee_label"
|
||||
android:textColor="@color/text_tertiary"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@
|
|||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:text="@string/onboarding_balance_title"
|
||||
android:text="@string/common_balance_title"
|
||||
android:textColor="@color/text_tertiary"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
android:elevation="0dp"
|
||||
android:fontFamily="sans-serif-medium"
|
||||
android:letterSpacing="0.036"
|
||||
android:text="@string/onboarding_balance_title"
|
||||
android:text="@string/common_balance_title"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/text_secondary"
|
||||
android:textSize="14sp"
|
||||
|
|
|
|||
|
|
@ -1,19 +1,24 @@
|
|||
plugins {
|
||||
alias(deps.plugins.android.library)
|
||||
alias(deps.plugins.kotlin.android)
|
||||
alias(deps.plugins.kotlin.jvm)
|
||||
alias(deps.plugins.kotlin.kapt)
|
||||
alias(deps.plugins.hilt.android)
|
||||
id("configuration")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
/** DI */
|
||||
implementation(deps.hilt.android)
|
||||
implementation(deps.hilt.core)
|
||||
kapt(deps.hilt.kapt)
|
||||
|
||||
/** Core shouldn't depends on core, but in case with utils and logging its necessary */
|
||||
implementation(projects.core.utils)
|
||||
|
||||
/** Analytics - Models */
|
||||
implementation(projects.core.analytics.models)
|
||||
|
||||
/** Domain */
|
||||
implementation(projects.domain.analytics)
|
||||
|
||||
/** Other */
|
||||
implementation(deps.kotlin.coroutines)
|
||||
|
||||
/** Core shouldn't depend on core, but in case with utils and logging its necessary */
|
||||
implementation(projects.core.utils)
|
||||
}
|
||||
|
|
@ -8,4 +8,7 @@ open class AnalyticsEvent(
|
|||
val event: String,
|
||||
var params: Map<String, String> = mapOf(),
|
||||
val error: Throwable? = null,
|
||||
)
|
||||
) {
|
||||
|
||||
val id: String = "[$category] $event"
|
||||
}
|
||||
|
|
@ -114,6 +114,11 @@ sealed class AnalyticsParam {
|
|||
object SeedImport : WalletCreationType("Seed import")
|
||||
}
|
||||
|
||||
sealed class WalletType(val value: String) {
|
||||
object MultiCurrency : WalletType(value = "Multicurrency")
|
||||
class SingleCurrency(currencyName: String) : WalletType(currencyName)
|
||||
}
|
||||
|
||||
companion object Key {
|
||||
const val BLOCKCHAIN = "blockchain"
|
||||
const val TOKEN = "Token"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.core.analytics.models
|
||||
|
||||
interface OneTimeAnalyticsEvent {
|
||||
|
||||
val oneTimeEventId: String
|
||||
}
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.tangem.core.analytics" />
|
||||
|
|
@ -16,7 +16,7 @@ interface AnalyticsEventFilter {
|
|||
* An internal filter check that, on external or internal conditions, recognizes the possibility of
|
||||
* sending an event.
|
||||
*/
|
||||
fun canBeSent(event: AnalyticsEvent): Boolean
|
||||
suspend fun canBeSent(event: AnalyticsEvent): Boolean
|
||||
|
||||
/**
|
||||
* Performs a check to see if the event can be dispatched by a specific handler
|
||||
|
|
|
|||
|
|
@ -12,13 +12,11 @@ interface AnalyticsEventHandler {
|
|||
interface AnalyticsHandler : AnalyticsEventHandler {
|
||||
fun id(): String
|
||||
|
||||
fun send(event: String, params: Map<String, String> = emptyMap())
|
||||
fun send(eventId: String, params: Map<String, String> = emptyMap())
|
||||
|
||||
override fun send(event: AnalyticsEvent) {
|
||||
send(prepareEventString(event), event.params)
|
||||
send(event.id, event.params)
|
||||
}
|
||||
|
||||
fun prepareEventString(event: AnalyticsEvent): String = "[${event.category}] ${event.event}"
|
||||
}
|
||||
|
||||
interface ErrorEventHandler {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ package com.tangem.core.analytics.di
|
|||
|
||||
import com.tangem.core.analytics.Analytics
|
||||
import com.tangem.core.analytics.api.AnalyticsEventHandler
|
||||
import com.tangem.core.analytics.filter.OneTimeEventFilter
|
||||
import com.tangem.domain.analytics.repository.AnalyticsRepository
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -10,11 +12,16 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
class AnalyticsModule {
|
||||
internal object AnalyticsModule {
|
||||
|
||||
@Singleton
|
||||
@Provides
|
||||
fun provideAnalyticsHandler(): AnalyticsEventHandler {
|
||||
return Analytics // todo replace after refactoring calling Analytics in whole project
|
||||
}
|
||||
|
||||
@Provides
|
||||
fun provideOneTimeEventFilter(analyticsRepository: AnalyticsRepository): OneTimeEventFilter {
|
||||
return OneTimeEventFilter(analyticsRepository)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package com.tangem.core.analytics.filter
|
||||
|
||||
import com.tangem.core.analytics.api.AnalyticsEventFilter
|
||||
import com.tangem.core.analytics.api.AnalyticsHandler
|
||||
import com.tangem.core.analytics.models.AnalyticsEvent
|
||||
import com.tangem.core.analytics.models.OneTimeAnalyticsEvent
|
||||
import com.tangem.domain.analytics.repository.AnalyticsRepository
|
||||
|
||||
class OneTimeEventFilter(
|
||||
private val analyticsRepository: AnalyticsRepository,
|
||||
) : AnalyticsEventFilter {
|
||||
|
||||
override fun canBeAppliedTo(event: AnalyticsEvent): Boolean = event is OneTimeAnalyticsEvent
|
||||
|
||||
override suspend fun canBeSent(event: AnalyticsEvent): Boolean {
|
||||
if (event !is OneTimeAnalyticsEvent) return true
|
||||
|
||||
val isSent = analyticsRepository.checkIsEventSent(event.oneTimeEventId)
|
||||
|
||||
if (!isSent) {
|
||||
analyticsRepository.setIsEventSent(event.oneTimeEventId)
|
||||
}
|
||||
|
||||
return !isSent
|
||||
}
|
||||
|
||||
override fun canBeConsumedByHandler(handler: AnalyticsHandler, event: AnalyticsEvent): Boolean {
|
||||
return canBeAppliedTo(event)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,11 @@ sealed class ApiResponseError : Exception() {
|
|||
* @property code The HTTP status code.
|
||||
* @property message A human-readable message describing the error.
|
||||
*/
|
||||
data class HttpException(val code: Code, override val message: String?) : ApiResponseError() {
|
||||
data class HttpException(
|
||||
val code: Code,
|
||||
override val message: String?,
|
||||
val errorBody: String?,
|
||||
) : ApiResponseError() {
|
||||
|
||||
// region Error Codes
|
||||
enum class Code(val code: Int) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ internal fun <T : Any> Response<T>.toSafeApiResponse(): ApiResponse<T> {
|
|||
val e = if (code == null) {
|
||||
ApiResponseError.UnknownException(IllegalArgumentException("Unknown error status code: ${code()}"))
|
||||
} else {
|
||||
ApiResponseError.HttpException(code, message())
|
||||
ApiResponseError.HttpException(code, message(), errorBody()?.string())
|
||||
}
|
||||
|
||||
apiError(e)
|
||||
|
|
|
|||
|
|
@ -8,13 +8,12 @@ import retrofit2.http.Body
|
|||
import retrofit2.http.GET
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Query
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* Interface of Tangem Express API (new swap mechanism)
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
interface ExpressApi {
|
||||
interface TangemExpressApi {
|
||||
|
||||
@POST("assets")
|
||||
suspend fun getAssets(@Body body: AssetsRequestBody): ApiResponse<List<Asset>>
|
||||
|
|
@ -31,9 +30,11 @@ interface ExpressApi {
|
|||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: BigDecimal,
|
||||
@Query("providerId") providerId: Int,
|
||||
@Query("rateType") rateType: RateType,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
@Query("rateType") rateType: String,
|
||||
): ApiResponse<ExchangeQuoteResponse>
|
||||
|
||||
@GET("exchange-data")
|
||||
|
|
@ -42,12 +43,14 @@ interface ExpressApi {
|
|||
@Query("fromNetwork") fromNetwork: String,
|
||||
@Query("toContractAddress") toContractAddress: String,
|
||||
@Query("toNetwork") toNetwork: String,
|
||||
@Query("fromAmount") fromAmount: BigDecimal,
|
||||
@Query("providerId") providerId: Int,
|
||||
@Query("rateType") rateType: RateType,
|
||||
@Query("fromAmount") fromAmount: String,
|
||||
@Query("fromDecimals") fromDecimals: Int,
|
||||
@Query("toDecimals") toDecimals: Int,
|
||||
@Query("providerId") providerId: String,
|
||||
@Query("rateType") rateType: String,
|
||||
@Query("toAddress") toAddress: String,
|
||||
): ApiResponse<ExchangeDataResponse>
|
||||
|
||||
@GET("exchange-result")
|
||||
suspend fun getExchangeResults(@Query("txId") txId: String): ApiResponse<ExchangeResultsResponse>
|
||||
@GET("exchange-status")
|
||||
suspend fun getExchangeStatus(@Query("txId") txId: String): ApiResponse<ExchangeStatusResponse>
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.datasource.api.express.models
|
||||
|
||||
object TangemExpressValues {
|
||||
const val EMPTY_CONTRACT_ADDRESS_VALUE = "0"
|
||||
}
|
||||
|
|
@ -3,5 +3,5 @@ package com.tangem.datasource.api.express.models.request
|
|||
import com.squareup.moshi.Json
|
||||
|
||||
data class AssetsRequestBody(
|
||||
@Json(name = "filter") val filter: List<LeastTokenInfo>?,
|
||||
@Json(name = "tokensList") val tokensList: List<LeastTokenInfo>?,
|
||||
)
|
||||
|
|
@ -9,21 +9,6 @@ data class Asset(
|
|||
@Json(name = "network")
|
||||
val network: String,
|
||||
|
||||
@Json(name = "token")
|
||||
val token: String,
|
||||
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
|
||||
@Json(name = "symbol")
|
||||
val symbol: String,
|
||||
|
||||
@Json(name = "decimals")
|
||||
val decimals: Int,
|
||||
|
||||
@Json(name = "isActive")
|
||||
val isActive: Boolean,
|
||||
|
||||
@Json(name = "exchangeAvailable")
|
||||
val exchangeAvailable: Boolean,
|
||||
)
|
||||
|
|
@ -4,8 +4,17 @@ import com.squareup.moshi.Json
|
|||
import java.math.BigDecimal
|
||||
|
||||
data class ExchangeDataResponse(
|
||||
@Json(name = "fromAmount")
|
||||
val fromAmount: String,
|
||||
|
||||
@Json(name = "fromDecimals")
|
||||
val fromDecimals: Int,
|
||||
|
||||
@Json(name = "toAmount")
|
||||
val toAmount: BigDecimal,
|
||||
val toAmount: String,
|
||||
|
||||
@Json(name = "toDecimals")
|
||||
val toDecimals: Int,
|
||||
|
||||
@Json(name = "txType")
|
||||
val txType: TxType,
|
||||
|
|
|
|||
|
|
@ -4,19 +4,19 @@ import com.squareup.moshi.Json
|
|||
|
||||
data class ExchangeProvider(
|
||||
@Json(name = "id")
|
||||
val id: Int,
|
||||
val id: String,
|
||||
|
||||
@Json(name = "name")
|
||||
val name: String,
|
||||
|
||||
@Json(name = "id")
|
||||
@Json(name = "type")
|
||||
val type: ExchangeProviderType,
|
||||
|
||||
@Json(name = "imageLarge")
|
||||
val imageLargeUrl: Int,
|
||||
val imageLargeUrl: String,
|
||||
|
||||
@Json(name = "imageSmall")
|
||||
val imageSmallUrl: Int,
|
||||
val imageSmallUrl: String,
|
||||
)
|
||||
|
||||
enum class ExchangeProviderType {
|
||||
|
|
|
|||
|
|
@ -4,9 +4,23 @@ import com.squareup.moshi.Json
|
|||
import java.math.BigDecimal
|
||||
|
||||
data class ExchangeQuoteResponse(
|
||||
|
||||
@Json(name = "fromAmount")
|
||||
val fromAmount: String,
|
||||
|
||||
@Json(name = "fromDecimals")
|
||||
val fromDecimals: Int,
|
||||
|
||||
@Json(name = "toAmount")
|
||||
val toAmount: BigDecimal,
|
||||
val toAmount: String,
|
||||
|
||||
@Json(name = "toDecimals")
|
||||
val toDecimals: Int,
|
||||
|
||||
@Json(name = "allowanceContract")
|
||||
val allowanceContract: String?,
|
||||
|
||||
@Json(name = "minAmount")
|
||||
val minAmount: BigDecimal,
|
||||
|
||||
)
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
data class ExchangeResultsResponse(
|
||||
@Json(name = "status")
|
||||
val status: ExchangeResultsStatus,
|
||||
|
||||
@Json(name = "externalStatus")
|
||||
val externalStatus: String,
|
||||
|
||||
@Json(name = "externalTxUrl")
|
||||
val externalTxUrl: String,
|
||||
|
||||
@Json(name = "error")
|
||||
val error: ExchangeResultsError?,
|
||||
)
|
||||
|
||||
enum class ExchangeResultsStatus {
|
||||
@Json(name = "processing")
|
||||
PROCESSING,
|
||||
|
||||
@Json(name = "done")
|
||||
DONE,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
|
||||
@Json(name = "refunded")
|
||||
REFUNDED,
|
||||
|
||||
@Json(name = "verificationRequired")
|
||||
VERIFICATION_REQUIRED,
|
||||
}
|
||||
|
||||
data class ExchangeResultsError(
|
||||
@Json(name = "code")
|
||||
val code: Int,
|
||||
|
||||
@Json(name = "description")
|
||||
val description: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
|
||||
data class ExchangeStatusResponse(
|
||||
|
||||
@Json(name = "providerId")
|
||||
val providerId: String,
|
||||
|
||||
@Json(name = "externalTxId")
|
||||
val externalTxId: String,
|
||||
|
||||
@Json(name = "externalTxStatus")
|
||||
val externalStatus: ExchangeStatus,
|
||||
|
||||
@Json(name = "externalTxUrl")
|
||||
val externalTxUrl: String,
|
||||
|
||||
@Json(name = "error")
|
||||
val error: ExchangeStatusError?,
|
||||
)
|
||||
|
||||
enum class ExchangeStatus {
|
||||
|
||||
@Json(name = "new")
|
||||
NEW,
|
||||
|
||||
@Json(name = "waiting")
|
||||
WAITING,
|
||||
|
||||
@Json(name = "confirming")
|
||||
CONFIRMING,
|
||||
|
||||
@Json(name = "exchanging")
|
||||
EXCHANGING,
|
||||
|
||||
@Json(name = "sending")
|
||||
SENDING,
|
||||
|
||||
@Json(name = "finished")
|
||||
FINISHED,
|
||||
|
||||
@Json(name = "failed")
|
||||
FAILED,
|
||||
|
||||
@Json(name = "refunded")
|
||||
REFUNDED,
|
||||
|
||||
@Json(name = "verifying")
|
||||
VERIFYING,
|
||||
}
|
||||
|
||||
data class ExchangeStatusError(
|
||||
@Json(name = "code")
|
||||
val code: Int,
|
||||
|
||||
@Json(name = "description")
|
||||
val description: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import java.math.BigDecimal
|
||||
|
||||
data class ExpressErrorResponse(
|
||||
@Json(name = "error")
|
||||
val error: ExpressError,
|
||||
)
|
||||
|
||||
data class ExpressError(
|
||||
@Json(name = "code")
|
||||
val code: Int,
|
||||
|
||||
@Json(name = "description")
|
||||
val description: String?,
|
||||
|
||||
@Json(name = "value")
|
||||
val value: ExpressErrorValue?,
|
||||
)
|
||||
|
||||
data class ExpressErrorValue(
|
||||
@Json(name = "minAmount")
|
||||
val minAmount: String?,
|
||||
|
||||
@Json(name = "decimals")
|
||||
val decimals: Int?,
|
||||
|
||||
@Json(name = "currentAllowance")
|
||||
val currentAllowance: BigDecimal?,
|
||||
|
||||
@Json(name = "receivedFromDecimals")
|
||||
val receivedFromDecimals: Int?,
|
||||
|
||||
@Json(name = "expressFromDecimals")
|
||||
val expressFromDecimals: Int?,
|
||||
)
|
||||
|
|
@ -17,10 +17,10 @@ data class SwapPair(
|
|||
|
||||
data class SwapPairProvider(
|
||||
@Json(name = "providerId")
|
||||
val providerId: Int,
|
||||
val providerId: String,
|
||||
|
||||
@Json(name = "rateType")
|
||||
val rateType: RateType,
|
||||
@Json(name = "rateTypes")
|
||||
val rateTypes: List<RateType>,
|
||||
)
|
||||
|
||||
enum class RateType {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package com.tangem.datasource.api.express.models.response
|
||||
|
||||
class SwapPairsWithProviders(
|
||||
val swapPair: List<SwapPair>,
|
||||
val providers: List<ExchangeProvider>,
|
||||
)
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.token.DefaultUserMarketCoinsStore
|
||||
import com.tangem.datasource.local.token.UserMarketCoinsStore
|
||||
import com.tangem.datasource.local.token.DefaultAssetsStore
|
||||
import com.tangem.datasource.local.token.AssetsStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
|
@ -11,11 +11,11 @@ import javax.inject.Singleton
|
|||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
internal object MarketCoinsStoreModule {
|
||||
internal object AssetsStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideUserMarketCoinsStore(): UserMarketCoinsStore {
|
||||
return DefaultUserMarketCoinsStore(dataStore = RuntimeDataStore())
|
||||
fun provideAssetsStore(): AssetsStore {
|
||||
return DefaultAssetsStore(dataStore = RuntimeDataStore())
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,9 @@ package com.tangem.datasource.di
|
|||
|
||||
import android.content.Context
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.tangem.datasource.BuildConfig
|
||||
import com.tangem.datasource.api.common.response.ApiResponseCallAdapterFactory
|
||||
import com.tangem.datasource.api.express.ExpressApi
|
||||
import com.tangem.datasource.api.express.TangemExpressApi
|
||||
import com.tangem.datasource.api.promotion.PromotionApi
|
||||
import com.tangem.datasource.api.tangemTech.TangemTechApi
|
||||
import com.tangem.datasource.utils.RequestHeader.*
|
||||
|
|
@ -32,11 +33,16 @@ class NetworkModule {
|
|||
@NetworkMoshi moshi: Moshi,
|
||||
@ApplicationContext context: Context,
|
||||
expressAuthProvider: ExpressAuthProvider,
|
||||
): ExpressApi {
|
||||
): TangemExpressApi {
|
||||
val url = if (BuildConfig.ENVIRONMENT == "dev") {
|
||||
DEV_EXPRESS_BASE_URL
|
||||
} else {
|
||||
PROD_EXPRESS_BASE_URL
|
||||
}
|
||||
return Retrofit.Builder()
|
||||
.addConverterFactory(MoshiConverterFactory.create(moshi))
|
||||
.addCallAdapterFactory(ApiResponseCallAdapterFactory.create())
|
||||
.baseUrl(DEV_EXPRESS_BASE_URL)
|
||||
.baseUrl(url)
|
||||
.client(
|
||||
OkHttpClient.Builder()
|
||||
.addHeaders(Express(expressAuthProvider))
|
||||
|
|
@ -44,7 +50,7 @@ class NetworkModule {
|
|||
.build(),
|
||||
)
|
||||
.build()
|
||||
.create(ExpressApi::class.java)
|
||||
.create(TangemExpressApi::class.java)
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
package com.tangem.datasource.di
|
||||
|
||||
import com.tangem.datasource.local.datastore.RuntimeDataStore
|
||||
import com.tangem.datasource.local.swaptx.DefaultSwapTransactionStatusStore
|
||||
import com.tangem.datasource.local.swaptx.SwapTransactionStatusStore
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object SwapTransactionStatusStoreModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSwapTransactionStatusStore(): SwapTransactionStatusStore {
|
||||
return DefaultSwapTransactionStatusStore(
|
||||
dataStore = RuntimeDataStore(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -44,12 +44,20 @@ class AppPreferencesStore(
|
|||
return this[key]?.let(adapter::fromJson)
|
||||
}
|
||||
|
||||
/** Get nullable list of data [T] by string [key] */
|
||||
/** Get list of data [T] by string [key] */
|
||||
inline fun <reified T> MutablePreferences.getObjectList(key: Preferences.Key<String>): List<T>? {
|
||||
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||
return this[key]?.let(adapter::fromJson)
|
||||
}
|
||||
|
||||
/** Get map with [String] key and value [V] by string [key] from [MutablePreferences] */
|
||||
inline fun <reified V> MutablePreferences.getObjectMap(key: Preferences.Key<String>): Map<String, V>? {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
return this[key]?.let(adapter::fromJson)
|
||||
}
|
||||
|
||||
/**
|
||||
* Set data [T] by string [key] to [MutablePreferences]
|
||||
*
|
||||
|
|
@ -67,4 +75,12 @@ class AppPreferencesStore(
|
|||
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||
this[key] = adapter.toJson(value)
|
||||
}
|
||||
|
||||
/** Set map with [String] key and value [V] by string [key] to [MutablePreferences] */
|
||||
inline fun <reified V> MutablePreferences.setObjectMap(key: Preferences.Key<String>, value: Map<String, V>) {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
this[key] = adapter.toJson(value)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,16 @@ object PreferencesKeys {
|
|||
val SELECTED_APP_CURRENCY_KEY by lazy { stringPreferencesKey(name = "selectedAppCurrency") }
|
||||
|
||||
val BALANCE_HIDING_SETTINGS_KEY by lazy { stringPreferencesKey(name = "balanceHidingSettings") }
|
||||
|
||||
val SWAP_TRANSACTIONS_KEY by lazy { stringPreferencesKey(name = "swapTransactions") }
|
||||
|
||||
val WALLETS_SCROLL_PREVIEW_KEY by lazy { booleanPreferencesKey(name = "walletsScrollPreview") }
|
||||
|
||||
val SENT_ONE_TIME_EVENTS_KEY by lazy { stringPreferencesKey(name = "sentOneTimeEvents") }
|
||||
|
||||
val WALLETS_BALANCES_STATES_KEY by lazy { stringPreferencesKey(name = "walletsBalancesStates") }
|
||||
|
||||
val LAST_SWAPPED_CRYPTOCURRENCY_ID_KEY by lazy { stringPreferencesKey(name = "lastSwappedCryptoCurrency") }
|
||||
}
|
||||
|
||||
/** Preferences keys set that should be migrated from "PreferencesDataSource" to a new DataStore<Preferences> */
|
||||
|
|
|
|||
|
|
@ -103,9 +103,31 @@ inline fun <reified T> AppPreferencesStore.getObjectList(key: Preferences.Key<St
|
|||
return data.map { it[key]?.let(adapter::fromJson) }
|
||||
}
|
||||
|
||||
/** Get nullable list of data [T] by string [key] */
|
||||
/** Get list of data [T] by string [key], or empty if data is not found */
|
||||
suspend inline fun <reified T> AppPreferencesStore.getObjectListSync(key: Preferences.Key<String>): List<T> {
|
||||
val adapter = moshi.adapter<List<T>>(Types.newParameterizedType(List::class.java, T::class.java))
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
.orEmpty()
|
||||
}
|
||||
|
||||
/** Store map with [String] key and value [V] by string [key] */
|
||||
suspend inline fun <reified V> AppPreferencesStore.storeObjectMap(
|
||||
key: Preferences.Key<String>,
|
||||
value: Map<String, V>,
|
||||
) {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
edit { it[key] = adapter.toJson(value) }
|
||||
}
|
||||
|
||||
/** Get map with [String] key and value [V] by string [key], or empty if data is not found */
|
||||
suspend inline fun <reified V> AppPreferencesStore.getObjectMap(key: Preferences.Key<String>): Map<String, V> {
|
||||
val type = Types.newParameterizedType(Map::class.java, String::class.java, V::class.java)
|
||||
val adapter = moshi.adapter<Map<String, V>>(type)
|
||||
|
||||
return data.firstOrNull()
|
||||
?.get(key)
|
||||
?.let(adapter::fromJson)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
package com.tangem.datasource.local.swaptx
|
||||
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
|
||||
internal class DefaultSwapTransactionStatusStore(
|
||||
private val dataStore: StringKeyDataStore<ExchangeAnalyticsStatus>,
|
||||
) : SwapTransactionStatusStore, StringKeyDataStore<ExchangeAnalyticsStatus> by dataStore {
|
||||
|
||||
override suspend fun getTransactionStatus(txId: String) = getSyncOrNull(txId)
|
||||
|
||||
override suspend fun setTransactionStatus(txId: String, status: ExchangeAnalyticsStatus) = store(txId, status)
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
package com.tangem.datasource.local.swaptx
|
||||
|
||||
/**
|
||||
* Runtime cache for storing swap transactions statuses sent to analytics
|
||||
*/
|
||||
interface SwapTransactionStatusStore {
|
||||
suspend fun getTransactionStatus(txId: String): ExchangeAnalyticsStatus?
|
||||
|
||||
suspend fun setTransactionStatus(txId: String, status: ExchangeAnalyticsStatus)
|
||||
}
|
||||
|
||||
enum class ExchangeAnalyticsStatus(val value: String) {
|
||||
InProgress("In Progress"),
|
||||
Done("Done"),
|
||||
Fail("Fail"),
|
||||
KYC("KYC"),
|
||||
Refunded("Refunded"),
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.api.express.models.response.Asset
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface AssetsStore {
|
||||
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId): List<Asset>?
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, item: List<Asset>)
|
||||
}
|
||||
|
|
@ -1,18 +1,18 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.datasource.api.express.models.response.Asset
|
||||
import com.tangem.datasource.local.datastore.core.StringKeyDataStore
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
internal class DefaultUserMarketCoinsStore(
|
||||
private val dataStore: StringKeyDataStore<CoinsResponse>,
|
||||
) : UserMarketCoinsStore {
|
||||
internal class DefaultAssetsStore(
|
||||
private val dataStore: StringKeyDataStore<List<Asset>>,
|
||||
) : AssetsStore {
|
||||
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse? {
|
||||
override suspend fun getSyncOrNull(userWalletId: UserWalletId): List<Asset>? {
|
||||
return dataStore.getSyncOrNull(userWalletId.stringValue)
|
||||
}
|
||||
|
||||
override suspend fun store(userWalletId: UserWalletId, item: CoinsResponse) {
|
||||
override suspend fun store(userWalletId: UserWalletId, item: List<Asset>) {
|
||||
dataStore.store(userWalletId.stringValue, item)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
package com.tangem.datasource.local.token
|
||||
|
||||
import com.tangem.datasource.api.tangemTech.models.CoinsResponse
|
||||
import com.tangem.domain.wallets.models.UserWalletId
|
||||
|
||||
interface UserMarketCoinsStore {
|
||||
|
||||
suspend fun getSyncOrNull(userWalletId: UserWalletId): CoinsResponse?
|
||||
|
||||
suspend fun store(userWalletId: UserWalletId, item: CoinsResponse)
|
||||
}
|
||||
|
|
@ -34,5 +34,9 @@
|
|||
{
|
||||
"name": "REDESIGNED_SEND_SCREEN_ENABLED",
|
||||
"version": "undefined"
|
||||
},
|
||||
{
|
||||
"name": "WALLETS_SCROLLING_PREVIEW_ENABLED",
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Erhalt der Gebühr fehlgeschlagen</string>
|
||||
<string name="no_account_generic">Laden Sie %1$s+ %2$s auf um ein Konto zu erstellen</string>
|
||||
<string name="send_error_dust_amount_format">Minimaler Betrag ist %s</string>
|
||||
<string name="send_error_dust_change">Restbestand zu klein</string>
|
||||
<string name="send_error_invalid_fee_value">Falsche Gebühr</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="common_save_changes">Änderungen speichern</string>
|
||||
<string name="common_send">Absenden</string>
|
||||
<string name="common_success">Erfolg</string>
|
||||
<string name="common_fee_label">Gebühr</string>
|
||||
<string name="details_manage_security_access_code">Zugangscode</string>
|
||||
<string name="details_manage_security_access_code_description">Sie müssen den richtigen Zugangscode eingeben, bevor Sie die Karte scannen.</string>
|
||||
<string name="details_manage_security_long_tap">Langes Tippen</string>
|
||||
|
|
@ -40,7 +41,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">inkl. Gebühr</string>
|
||||
<string name="send_fee_label">Gebühr</string>
|
||||
<string name="send_fee_picker_low">Niedrig</string>
|
||||
<string name="send_fee_picker_normal">Normal</string>
|
||||
<string name="send_fee_picker_priority">Priorität</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Échec de réception des commissions</string>
|
||||
<string name="no_account_generic">Pour créer un compte, téléchargez %1$s+ %2$s</string>
|
||||
<string name="send_error_dust_amount_format">Le montant minimal est de %s</string>
|
||||
<string name="send_error_dust_change">Le reste est trop petit</string>
|
||||
<string name="send_error_invalid_fee_value">Commission non valide</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="common_save_changes">Sauvegarder les modifications</string>
|
||||
<string name="common_send">Envoyer</string>
|
||||
<string name="common_success">Avec succès</string>
|
||||
<string name="common_fee_label">Commissions</string>
|
||||
<string name="details_manage_security_access_code">Code d\'accès</string>
|
||||
<string name="details_manage_security_access_code_description">Vous devrez entrer le mot de passe correct avant de scanner la carte</string>
|
||||
<string name="details_manage_security_long_tap">Tenez la carte fermement</string>
|
||||
|
|
@ -40,7 +41,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Inclure les commissions</string>
|
||||
<string name="send_fee_label">Commissions</string>
|
||||
<string name="send_fee_picker_low">Bas</string>
|
||||
<string name="send_fee_picker_normal">Normal</string>
|
||||
<string name="send_fee_picker_priority">Priorité</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<resources>
|
||||
<string name="common_fee_error">Impossibile ottenere la commissione</string>
|
||||
<string name="no_account_generic">Scarica %1$s+ %2$s per creare un account</string>
|
||||
<string name="send_error_dust_amount_format">L\'importo minimo è di %s</string>
|
||||
<string name="send_error_dust_change">L\'importo residuo è molto basso</string>
|
||||
<string name="send_error_invalid_fee_value">Commissione non valida</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
<string name="common_save_changes">Mantieni le modifiche</string>
|
||||
<string name="common_send">Invia</string>
|
||||
<string name="common_success">Con successo</string>
|
||||
<string name="common_fee_label">Commissione</string>
|
||||
<string name="details_manage_security_access_code">Codice di accesso</string>
|
||||
<string name="details_manage_security_access_code_description">Prima di scansionare la carta sarà necessario inserire il codice di accesso corretto</string>
|
||||
<string name="details_manage_security_long_tap">Mantenimento della carta</string>
|
||||
|
|
@ -40,7 +41,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Includi commissione</string>
|
||||
<string name="send_fee_label">Commissione</string>
|
||||
<string name="send_fee_picker_low">Insufficiente</string>
|
||||
<string name="send_fee_picker_normal">Normale</string>
|
||||
<string name="send_fee_picker_priority">Prioritario</string>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
<string name="common_approval">Одобрение</string>
|
||||
<string name="common_attention">Внимание</string>
|
||||
<string name="common_balance">Баланс: %s</string>
|
||||
<string name="common_balance_title">Баланс</string>
|
||||
<string name="common_biometric_authentication">биометрическую аутентификацию</string>
|
||||
<string name="common_biometrics">биометрией</string>
|
||||
<string name="common_buy">Купить</string>
|
||||
|
|
@ -85,9 +86,11 @@
|
|||
<string name="common_enabled">Включено</string>
|
||||
<string name="common_error">Ошибка</string>
|
||||
<string name="common_exchange">Обменять</string>
|
||||
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
|
||||
<string name="common_explore">Обозреватель</string>
|
||||
<string name="common_explore_history">Посмотреть историю</string>
|
||||
<string name="common_explore_transaction_history">Посмотреть историю транзакций</string>
|
||||
<string name="common_explorer">Обозреватель</string>
|
||||
<string name="common_fee_label">Комиссия</string>
|
||||
<string name="common_fee_selector_footer">Сетевые комиссии за транзакции используются для поддержки безопасности сети, поощрения валидаторов, выделения ресурсов и определения приоритета транзакции.</string>
|
||||
<string name="common_fee_selector_option_custom">Свое</string>
|
||||
<string name="common_fee_selector_option_fast">Быстро</string>
|
||||
|
|
@ -191,9 +194,19 @@
|
|||
<string name="exchange_tokens_available_tokens_header">Мои токены</string>
|
||||
<string name="exchange_tokens_empty_tokens">У вас нет добавленных токенов. Добавьте токены для обмена</string>
|
||||
<string name="exchange_tokens_unavailable_tokens_header">Недоступен для обмена с %s</string>
|
||||
<string name="express_cex_status_button_title">Статус</string>
|
||||
<string name="express_choose_providers_subtitle">Провайдеры проводят транзакции, обеспечивая плавный и эффективный обмен токенами</string>
|
||||
<string name="express_choose_providers_title">Выберите провайдера</string>
|
||||
<string name="express_exchange_notification_failed_text">Чтобы узнать причину, посетите сайт провайдера</string>
|
||||
<string name="express_exchange_notification_failed_title">Чтобы вернуть ваши деньги, посетите сайт провайдера</string>
|
||||
<string name="express_exchange_notification_verification_text">Посетите сайт провайдера для проверки</string>
|
||||
<string name="express_exchange_notification_verification_title">Провайдер запрашивает прохождение верификации</string>
|
||||
<string name="express_exchange_token_list_subtitle">Список токенов в вашем кошельке</string>
|
||||
<string name="express_fetch_best_rates">Получение наилучших курсов...</string>
|
||||
<string name="express_provider">Провайдер</string>
|
||||
<string name="express_provider_best_rate">Лучший курс</string>
|
||||
<string name="express_provider_min_amount">Доступно с %s</string>
|
||||
<string name="express_provider_not_available">Недоступно для этой пары</string>
|
||||
<string name="express_provider_permission_needed">Требуется разрешение</string>
|
||||
<string name="feedback_data_collection_message">Информация ниже не является обязательной. Вы можете стереть её, если хотите.</string>
|
||||
<string name="feedback_preface_rate_negative">Расскажите, каких функций вам не хватает, и мы постараемся вам помочь.</string>
|
||||
|
|
@ -267,7 +280,6 @@
|
|||
<string name="onboarding_access_codes_doesnt_match">Введенные коды доступа не совпадают</string>
|
||||
<string name="onboarding_alert_message_not_max_backup_cards_added">Вы добавили одну резервную карту. После того, как процесс будет завершен, Вы больше не сможете добавить карт. Если у Вас есть еще одна карта, добавьте ее в резервную копию. Хотите продолжить?</string>
|
||||
<string name="onboarding_backup_exit_warning">Процесс резервного копирования почти завершен. Вы не можете выйти из него сейчас.</string>
|
||||
<string name="onboarding_balance_title">Баланс</string>
|
||||
<string name="onboarding_button_add_backup_card">Добавить резервную карту</string>
|
||||
<string name="onboarding_button_backup_card_format">Сканировать карту #%d</string>
|
||||
<string name="onboarding_button_backup_now">Создать резервную копию</string>
|
||||
|
|
@ -418,7 +430,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">Включая комиссию</string>
|
||||
<string name="send_fee_label">Комиссия</string>
|
||||
<string name="send_fee_picker_low">Низкая</string>
|
||||
<string name="send_fee_picker_normal">Нормальная</string>
|
||||
<string name="send_fee_picker_priority">Приоритетная</string>
|
||||
|
|
@ -629,12 +640,18 @@
|
|||
<string name="warning_button_like_it">Нравится</string>
|
||||
<string name="warning_button_ok">Понятно!</string>
|
||||
<string name="warning_button_really_cool">Очень круто!</string>
|
||||
<string name="warning_button_refresh">Обновить</string>
|
||||
<string name="warning_demo_mode_message">Вы находитесь в режиме демо</string>
|
||||
<string name="warning_demo_mode_title">Демо режим включен</string>
|
||||
<string name="warning_developer_card_message">Отсканированная вами карта является картой разработчика. Не используйте ее для создания своего кошелька.</string>
|
||||
<string name="warning_developer_card_title">Не для пользователя!</string>
|
||||
<string name="warning_existential_deposit_message">Cеть %1$s использует концепцию экзистенциального депозита. Если баланс вашего счета будет ниже %2$s, то он будет деактивирован, а средства на счете уничтожены.</string>
|
||||
<string name="warning_existential_deposit_title">Для работы с сетью необходим депозит</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">У вас в списке нет монет доступных для обмена с %s</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">Нет доступных токенов для обмена</string>
|
||||
<string name="warning_express_refresh_required_title">Cервис временно недоступен</string>
|
||||
<string name="warning_express_too_minimal_amount_description">Пожалуйста, измените сумму для обмена</string>
|
||||
<string name="warning_express_too_minimal_amount_title">Сумма для обмена должна быть не менее %s</string>
|
||||
<string name="warning_failed_to_verify_card_message">Возможно, данная карта - образец или подделка</string>
|
||||
<string name="warning_failed_to_verify_card_title">Ошибка проверки подлинности</string>
|
||||
<string name="warning_low_signatures_message">На этой карте осталось всего %s подписей. Вам следует вывести все ваши средства.</string>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
<string name="address_type_legacy">遺留資產</string>
|
||||
<string name="common_fee_error">獲取費用失敗</string>
|
||||
<string name="kaspa_withdrawal_message_warning">由於 Kaspa 的限制,只有%1$d UTXO 可以放入單次交易中。這意味著您只能發送%2$s或更少數量。您需要減少數量。</string>
|
||||
<string name="no_account_generic">加載 %1$s+ %2$s 以創建帳戶</string>
|
||||
<string name="no_account_polkadot">目標帳戶未激活。發送 %s 或更多以激活帳戶</string>
|
||||
<string name="send_error_dust_amount_format">最小數量是 %s</string>
|
||||
<string name="send_error_dust_change">更動太小</string>
|
||||
|
|
|
|||
|
|
@ -90,6 +90,7 @@
|
|||
<string name="common_submit">提交</string>
|
||||
<string name="common_success">成功</string>
|
||||
<string name="common_swap">交換</string>
|
||||
<string name="common_fee_label">費用</string>
|
||||
<string name="common_terms_and_conditions">條款和條件</string>
|
||||
<string name="common_transactions">交易</string>
|
||||
<string name="common_understand">我了解</string>
|
||||
|
|
@ -178,7 +179,7 @@
|
|||
<string name="onboarding_access_codes_doesnt_match">輸入的訪問密碼與初始訪問密碼不匹配</string>
|
||||
<string name="onboarding_alert_message_not_max_backup_cards_added">您已添加一張備用卡。備份過程完成後,您將無法添加更多備份卡。如果您還有一張卡,請將其添加到備份中。您想繼續備份過程嗎?</string>
|
||||
<string name="onboarding_backup_exit_warning">備份過程已部分完成。你現在不能退出</string>
|
||||
<string name="onboarding_balance_title">餘額</string>
|
||||
<string name="common_balance_title">餘額</string>
|
||||
<string name="onboarding_button_add_backup_card">添加備用卡</string>
|
||||
<string name="onboarding_button_backup_card_format">掃描卡片 #%d</string>
|
||||
<string name="onboarding_button_backup_now">立即備份</string>
|
||||
|
|
@ -294,7 +295,6 @@
|
|||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_fee_include_description">包含費用</string>
|
||||
<string name="send_fee_label">費用</string>
|
||||
<string name="send_fee_picker_low">低</string>
|
||||
<string name="send_fee_picker_normal">正常</string>
|
||||
<string name="send_fee_picker_priority">優先</string>
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@
|
|||
<string name="common_approval">Approval</string>
|
||||
<string name="common_attention">Attention</string>
|
||||
<string name="common_balance">Balance: %s</string>
|
||||
<string name="common_balance_title">Balance</string>
|
||||
<string name="common_biometric_authentication">biometric authentication</string>
|
||||
<string name="common_biometrics">biometrics</string>
|
||||
<string name="common_buy">Buy</string>
|
||||
|
|
@ -84,8 +85,10 @@
|
|||
<string name="common_error">Error</string>
|
||||
<string name="common_exchange">Exchange</string>
|
||||
<string name="common_explore">Explore</string>
|
||||
<string name="common_explore_history">Explore history</string>
|
||||
<string name="common_explore_transaction_history">Explore transaction history</string>
|
||||
<string name="common_explorer">Explorer</string>
|
||||
<string name="common_fee_label">Fee</string>
|
||||
<string name="common_fee_selector_footer">Network transaction fees are used to support network security, incentivize validators, allocate resources, and determine transaction priority.</string>
|
||||
<string name="common_fee_selector_option_custom">Custom</string>
|
||||
<string name="common_fee_selector_option_fast">Fast</string>
|
||||
|
|
@ -185,14 +188,46 @@
|
|||
<string name="disclaimer_title">Terms of Service</string>
|
||||
<string name="error_update_app">Oops, the current version of the application is not ready to work with this card, please check for updates.</string>
|
||||
<string name="error_wrong_wallet_tapped">You have used a card from another wallet. Tap the card associated with this wallet</string>
|
||||
<string name="exchange_receive_view_header">You Receive</string>
|
||||
<string name="exchange_send_view_header">You Send</string>
|
||||
<string name="exchange_receive_view_header">You receive</string>
|
||||
<string name="exchange_send_view_header">You send</string>
|
||||
<string name="exchange_tokens_available_tokens_header">My tokens</string>
|
||||
<string name="exchange_tokens_empty_tokens">You don\'t have any added tokens yet. Add tokens via Market to swap</string>
|
||||
<string name="exchange_tokens_unavailable_tokens_header">Unavailable for swap from %s</string>
|
||||
<string name="express_choose_providers_subtitle">Providers facilitate transactions, ensuring smooth and efficient token exchanges</string>
|
||||
<string name="express_choose_providers_title">Choose Provider</string>
|
||||
<string name="express_provider_best_rate">Best Rate</string>
|
||||
<string name="exchange_tokens_empty_tokens">You haven\'t added any tokens yet. Add tokens via Market to swap</string>
|
||||
<string name="exchange_tokens_unavailable_tokens_header">Cannot be swapped for %s</string>
|
||||
<string name="express_cex_status_button_title">Status</string>
|
||||
<string name="express_choose_providers_subtitle">Providers facilitate transactions, ensuring smooth and efficient token swaps</string>
|
||||
<string name="express_choose_providers_title">Choose provider</string>
|
||||
<string name="express_estimated_amount">Estimated amount</string>
|
||||
<string name="express_exchange_by">Exchange by %s</string>
|
||||
<string name="express_exchange_notification_failed_text">Visit provider’s website to refund your money</string>
|
||||
<string name="express_exchange_notification_failed_title">Operation failed by provider</string>
|
||||
<string name="express_exchange_notification_verification_text">Visit provider’s website for verification</string>
|
||||
<string name="express_exchange_notification_verification_title">KYC verification required by provider</string>
|
||||
<string name="express_exchange_status_confirmed">Confirmed</string>
|
||||
<string name="express_exchange_status_confirming">Confirming</string>
|
||||
<string name="express_exchange_status_confirming_active">Confirming…</string>
|
||||
<string name="express_exchange_status_exchanged">Exchanged</string>
|
||||
<string name="express_exchange_status_exchanging">Exchanging</string>
|
||||
<string name="express_exchange_status_exchanging_active">Exchanging…</string>
|
||||
<string name="express_exchange_status_failed">Failed</string>
|
||||
<string name="express_exchange_status_received">Deposit received</string>
|
||||
<string name="express_exchange_status_receiving">Awaiting deposit</string>
|
||||
<string name="express_exchange_status_receiving_active">Awaiting deposit…</string>
|
||||
<string name="express_exchange_status_refunded">Refunded</string>
|
||||
<string name="express_exchange_status_sending">Sending to you</string>
|
||||
<string name="express_exchange_status_sending_active">Sending to you…</string>
|
||||
<string name="express_exchange_status_sent">Sent</string>
|
||||
<string name="express_exchange_status_subtitle">Provider-sourced data. Estimated amount subject to change.</string>
|
||||
<string name="express_exchange_status_title">Exchange status</string>
|
||||
<string name="express_exchange_status_verified">Verified</string>
|
||||
<string name="express_exchange_status_verifying">Verification required</string>
|
||||
<string name="express_exchange_token_list_subtitle">List of all tokens added to your wallet</string>
|
||||
<string name="express_fetch_best_rates">Fetching best rates...</string>
|
||||
<string name="express_floating_rate">Floating rate</string>
|
||||
<string name="express_go_to_provider">Go to provider</string>
|
||||
<string name="express_provider">Provider</string>
|
||||
<string name="express_provider_best_rate">Best rate</string>
|
||||
<string name="express_provider_min_amount">Available from %s</string>
|
||||
<string name="express_provider_not_available">Unavailable for this pair</string>
|
||||
<string name="express_provider_permission_needed">Permission Needed</string>
|
||||
<string name="feedback_data_collection_message">The following information is optional. You can erase it if you don\'t want to share it.</string>
|
||||
<string name="feedback_preface_rate_negative">Tell us what functions you are missing, and we will try to help you.</string>
|
||||
|
|
@ -267,7 +302,6 @@
|
|||
<string name="onboarding_access_codes_doesnt_match">Entered access code didn\'t match the initial access code</string>
|
||||
<string name="onboarding_alert_message_not_max_backup_cards_added">You\'ve added one backup card. When backup process is finished you can\'t add more backup cards. If you have one more card, add it to backup. Do you like to continue the backup process?</string>
|
||||
<string name="onboarding_backup_exit_warning">The backup process is partly complete. You can\'t exit it now.</string>
|
||||
<string name="onboarding_balance_title">Balance</string>
|
||||
<string name="onboarding_button_add_backup_card">Add a backup card</string>
|
||||
<string name="onboarding_button_backup_card_format">Scan the card #%d</string>
|
||||
<string name="onboarding_button_backup_now">Backup now</string>
|
||||
|
|
@ -413,9 +447,7 @@
|
|||
<string name="send_extras_error_invalid_memo">Invalid Memo. It won\'t be added to the transaction.</string>
|
||||
<string name="send_extras_hint_destination_tag">Tag</string>
|
||||
<string name="send_extras_hint_memo">Memo</string>
|
||||
<string name="send_insufficient_funds">Insufficient funds for transfer</string>
|
||||
<string name="send_fee_include_description">Include fee</string>
|
||||
<string name="send_fee_label">Fee</string>
|
||||
<string name="send_fee_picker_low">Low</string>
|
||||
<string name="send_fee_picker_normal">Normal</string>
|
||||
<string name="send_fee_picker_priority">Priority</string>
|
||||
|
|
@ -490,7 +522,7 @@
|
|||
<string name="story_meet_title">Meet Tangem</string>
|
||||
<string name="story_web3_description">Exchange, buy NFT\'s, make loans and deposits in more than 100 different decentralized services</string>
|
||||
<string name="story_web3_title">Web 3.0 Compatible</string>
|
||||
<string name="swapping_approve_information_text">Approvals are considered an industry standard across all decentralized exchanges and protect your wallet from being accessed by a smart contract without your permission. By design, smart contracts can\'t access your tokens unless you approve access from your end. By \"unlocking\" your tokens, you are give permission to the 1inch smart contract to spend your assets. The miners of the network are compensated with a gas fee (paid by you) to record this action on the blockchain. Once permission has been granted you will be able to swap your token.</string>
|
||||
<string name="swapping_approve_information_text">All decentralized exchanges require approvals to prevent smart contracts from accessing your wallet without your permission. By design, smart contracts can\'t access your tokens unless you approve. By \"unlocking\" your tokens, you authorize the 1-inch smart contract to spend them. The network\'s miners receive a gas fee (paid by you) to record this action on the blockchain. You can swap your token after giving approval.</string>
|
||||
<string name="swapping_approve_information_title">Approve</string>
|
||||
<string name="swapping_error_wrapper">Error: %s</string>
|
||||
<string name="swapping_generic_error">There was an error. Please try again.</string>
|
||||
|
|
@ -498,23 +530,25 @@
|
|||
<string name="swapping_high_price_impact">High price impact!</string>
|
||||
<string name="swapping_high_price_impact_description">Swapping this amount of selected tokens will cause a significant price impact and reduce your outcome.</string>
|
||||
<string name="swapping_insufficient_funds">Insufficient funds</string>
|
||||
<string name="swapping_not_enough_funds_for_fee">Not enough funds for fee in your %1$s wallet to create a transaction. Top up your %2$s wallet first.</string>
|
||||
<string name="swapping_not_enough_funds_for_fee">Insufficient funds in your %1$s wallet to cover fees. Top up your %2$s wallet first.</string>
|
||||
<string name="swapping_pending_transaction_subtitle">Transaction in progress...</string>
|
||||
<string name="swapping_pending_transaction_title">Waiting</string>
|
||||
<string name="swapping_permission_buttons_approve">Approve</string>
|
||||
<string name="swapping_permission_current_transaction">Current transaction</string>
|
||||
<string name="swapping_permission_fee_footer">The token approval network fee will be charged to confirm that you are the one allowing your token to be used for the exchange.</string>
|
||||
<string name="swapping_permission_fee_footer">The network will charge a token approval fee to verify that you are authorizing the use of your token for the swap.</string>
|
||||
<string name="swapping_permission_header">Give Permission</string>
|
||||
<string name="swapping_permission_policy_type_footer">Specify the approve limit for the selected token</string>
|
||||
<string name="swapping_permission_rows_amount">Amount %s</string>
|
||||
<string name="swapping_permission_rows_spender">Spender</string>
|
||||
<string name="swapping_permission_rows_your_wallet">Your Wallet</string>
|
||||
<string name="swapping_permission_subheader">To continue you need to allow 1inch smart contracts to use your %s</string>
|
||||
<string name="swapping_permission_rows_your_wallet">Your wallet</string>
|
||||
<string name="swapping_permission_subheader">To continue, grant 1inch smart contracts permission to use your %s</string>
|
||||
<string name="swapping_permission_unlimited">Unlimited</string>
|
||||
<string name="swapping_success_from_title">You swap</string>
|
||||
<string name="swapping_success_to_title">You receive</string>
|
||||
<string name="swapping_success_view_explorer_button_title">View in Explorer</string>
|
||||
<string name="swapping_success_view_title">In progress</string>
|
||||
<string name="swapping_swap_action">Swap</string>
|
||||
<string name="swapping_swap_of_to">Swap of %s to</string>
|
||||
<string name="swapping_swap_of_to">Swap %s for</string>
|
||||
<string name="swapping_tangem_fee_disclaimer">Quotes include an additional Tangem commission of %s. This helps us deliver a top-of-the-line product.</string>
|
||||
<string name="swapping_token_list_other_tokens">Other tokens</string>
|
||||
<string name="swapping_token_list_title">Choose token</string>
|
||||
|
|
@ -636,12 +670,20 @@
|
|||
<string name="warning_button_like_it">Like it</string>
|
||||
<string name="warning_button_ok">Ok, Got it!</string>
|
||||
<string name="warning_button_really_cool">Really cool!</string>
|
||||
<string name="warning_button_refresh">Refresh</string>
|
||||
<string name="warning_demo_mode_message">You are currently in the Demo mode</string>
|
||||
<string name="warning_demo_mode_title">Demo mode active</string>
|
||||
<string name="warning_developer_card_message">The card you scanned is a developer card. Do not use it to create your wallet.</string>
|
||||
<string name="warning_developer_card_title">Not for users!</string>
|
||||
<string name="warning_existential_deposit_message">%1$s network requires an Existential Deposit. If your account drops below %2$s, it will be deactivated, and any remaining funds will be destroyed.</string>
|
||||
<string name="warning_existential_deposit_title">Network requires Existential Deposit</string>
|
||||
<string name="warning_express_no_exchangeable_coins_description">You do not have any %s exchangeable coins in your list</string>
|
||||
<string name="warning_express_no_exchangeable_coins_title">No available tokens to swap</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_description">To make a transaction you need to deposit some %1$s %2$s</string>
|
||||
<string name="warning_express_not_enough_fee_for_token_tx_title">Unable to cover %s fee</string>
|
||||
<string name="warning_express_refresh_required_title">Service temporary unavailable</string>
|
||||
<string name="warning_express_too_minimal_amount_description">Please change the amount to swap</string>
|
||||
<string name="warning_express_too_minimal_amount_title">The amount to swap must be at least %s</string>
|
||||
<string name="warning_failed_to_verify_card_message">This card might be a production sample or counterfeit</string>
|
||||
<string name="warning_failed_to_verify_card_title">Authenticity check failed</string>
|
||||
<string name="warning_low_signatures_message">Only %s signatures are left on this card. You must withdraw all of your funds.</string>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import androidx.compose.material.Text
|
|||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
|
|
@ -162,11 +163,12 @@ fun CardWithIcon(
|
|||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun IconWithTitleAndDescription(
|
||||
internal fun IconWithTitleAndDescription(
|
||||
title: String,
|
||||
description: String,
|
||||
description: String?,
|
||||
icon: @Composable () -> Unit,
|
||||
additionalContent: @Composable () -> Unit = {},
|
||||
iconBackground: Color = TangemTheme.colors.background.secondary,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
|
|
@ -182,7 +184,7 @@ fun IconWithTitleAndDescription(
|
|||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.secondary,
|
||||
color = iconBackground,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.height(TangemTheme.dimens.size40)
|
||||
|
|
@ -205,12 +207,14 @@ fun IconWithTitleAndDescription(
|
|||
color = TangemTheme.colors.text.primary1,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
)
|
||||
SpacerH4()
|
||||
Text(
|
||||
text = description,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
if (description != null) {
|
||||
SpacerH4()
|
||||
Text(
|
||||
text = description,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
style = TangemTheme.typography.body2,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
additionalContent()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
|
|
@ -18,10 +17,12 @@ import androidx.compose.ui.text.style.TextAlign
|
|||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import com.tangem.core.ui.components.atoms.text.BoundCounter
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
|
||||
*/
|
||||
@Deprecated("Use EllipsisText with TextEllipsis.Middle ellipsis instead")
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun MiddleEllipsisText(
|
||||
|
|
@ -138,38 +139,4 @@ fun MiddleEllipsisText(
|
|||
|
||||
private const val ELLIPSIS_CHARACTERS_COUNT = 3
|
||||
private const val ELLIPSIS_CHARACTER = '.'
|
||||
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "")
|
||||
|
||||
private class BoundCounter(
|
||||
private val text: String,
|
||||
private val textLayoutResult: TextLayoutResult,
|
||||
private val charPosition: (Int) -> Int,
|
||||
) {
|
||||
var string = ""
|
||||
private set
|
||||
var width = 0f
|
||||
private set
|
||||
|
||||
private var _nextCharWidth: Float? = null
|
||||
private var invalidCharsCount = 0
|
||||
|
||||
fun widthWithNextChar(): Float = width + nextCharWidth()
|
||||
|
||||
private fun nextCharWidth(): Float = _nextCharWidth ?: run {
|
||||
var boundingBox: Rect
|
||||
// invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630
|
||||
invalidCharsCount--
|
||||
do {
|
||||
boundingBox = textLayoutResult
|
||||
.getBoundingBox(charPosition(string.count() + ++invalidCharsCount))
|
||||
} while (boundingBox.right == 0f)
|
||||
_nextCharWidth = boundingBox.width
|
||||
boundingBox.width
|
||||
}
|
||||
|
||||
fun addNextChar() {
|
||||
string += text[charPosition(string.count())]
|
||||
width += nextCharWidth()
|
||||
_nextCharWidth = null
|
||||
}
|
||||
}
|
||||
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "")
|
||||
|
|
@ -1,181 +0,0 @@
|
|||
package com.tangem.core.ui.components
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Screen for showing result
|
||||
*
|
||||
* @param resultMessage message to show
|
||||
* @param title title to show
|
||||
* @param resultColor color which will tint the round icon of the result
|
||||
* @param icon icon to show in the middle of the round icon
|
||||
* @param secondaryButtonIcon icon to show in the secondary button
|
||||
* @param secondaryButtonText label of the secondary button
|
||||
* @param onSecondaryButtonClick action on clicking secondary button
|
||||
* @param onButtonClick action on clicking "Done" button
|
||||
*
|
||||
* @see <a href =
|
||||
* "https://www.figma.com/file/Vs6SkVsFnUPsSCNwlnVf5U/Android-%E2%80%93-UI?node-id=1123%3A3863&t=wwR84h5IsMaMsDhq-1"
|
||||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun ResultScreenContent(
|
||||
resultMessage: AnnotatedString,
|
||||
onButtonClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
@StringRes title: Int = R.string.common_success,
|
||||
resultColor: Color = TangemTheme.colors.icon.accent,
|
||||
@DrawableRes icon: Int = R.drawable.ic_check_24,
|
||||
@DrawableRes secondaryButtonIcon: Int? = null,
|
||||
@StringRes secondaryButtonText: Int? = null,
|
||||
onSecondaryButtonClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.background(TangemTheme.colors.background.secondary)
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing16,
|
||||
vertical = TangemTheme.dimens.spacing32,
|
||||
),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center,
|
||||
) {
|
||||
SpacerHHalf()
|
||||
SuccessImage(resultColor = resultColor, icon = icon)
|
||||
SpacerH50()
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
style = TangemTheme.typography.h2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SpacerH12()
|
||||
Text(
|
||||
text = resultMessage,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
SpacerHHalf()
|
||||
if (onSecondaryButtonClick != null && secondaryButtonText != null) {
|
||||
SecondaryButtonForResultScreen(
|
||||
secondaryButtonText = secondaryButtonText,
|
||||
secondaryButtonIcon = secondaryButtonIcon,
|
||||
onSecondaryButtonClick = onSecondaryButtonClick,
|
||||
)
|
||||
SpacerH12()
|
||||
}
|
||||
PrimaryButton(
|
||||
text = stringResource(id = R.string.common_close),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
onClick = { onButtonClick() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SuccessImage(resultColor: Color, @DrawableRes icon: Int) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = resultColor.copy(alpha = 0.2f),
|
||||
shape = CircleShape,
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing24)
|
||||
.background(
|
||||
color = resultColor,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.height(TangemTheme.dimens.size93)
|
||||
.width(TangemTheme.dimens.size93),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.primary2,
|
||||
modifier = Modifier.size(TangemTheme.dimens.size40),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SecondaryButtonForResultScreen(
|
||||
@StringRes secondaryButtonText: Int,
|
||||
onSecondaryButtonClick: () -> Unit,
|
||||
@DrawableRes secondaryButtonIcon: Int? = null,
|
||||
) {
|
||||
if (secondaryButtonIcon != null) {
|
||||
SecondaryButtonIconStart(
|
||||
text = stringResource(id = secondaryButtonText),
|
||||
iconResId = secondaryButtonIcon,
|
||||
onClick = onSecondaryButtonClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
} else {
|
||||
SecondaryButton(
|
||||
text = stringResource(id = secondaryButtonText),
|
||||
onClick = onSecondaryButtonClick,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region preview
|
||||
|
||||
@Composable
|
||||
private fun SuccessScreenPreview() {
|
||||
ResultScreenContent(
|
||||
resultMessage = AnnotatedString("Swap of 1 000 DAI to 1 131,46 MATIC"),
|
||||
secondaryButtonText = R.string.swapping_success_view_explorer_button_title,
|
||||
onSecondaryButtonClick = {},
|
||||
onButtonClick = {},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_SuccessScreenContent_InLightTheme() {
|
||||
TangemTheme(isDark = false) {
|
||||
SuccessScreenPreview()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
private fun Preview_SuccessScreenContent_InDarkTheme() {
|
||||
TangemTheme(isDark = true) {
|
||||
SuccessScreenPreview()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion preview
|
||||
|
|
@ -23,7 +23,7 @@ import com.tangem.core.ui.res.TangemTheme
|
|||
@Composable
|
||||
fun TangemSwitch(
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
checkedColor: Color = TangemTheme.colors.icon.accent,
|
||||
checkedColor: Color = TangemTheme.colors.control.checked,
|
||||
uncheckedColor: Color = TangemTheme.colors.icon.informative,
|
||||
size: Dp = 48.dp,
|
||||
checked: Boolean = false,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import androidx.compose.material.ExperimentalMaterialApi
|
|||
import androidx.compose.material.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
|
|
@ -36,6 +37,28 @@ fun WarningCard(title: String, description: String, icon: @Composable (() -> Uni
|
|||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A card with a warning icon to the left and title without description shown to the right of it.
|
||||
*
|
||||
* @param title title of the warning in bold
|
||||
*
|
||||
* @see <a href = "https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?node-id=290%3A217&t=yMepJZTRh5bLkOoJ-1"
|
||||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun WarningCardTitleOnly(title: String, icon: @Composable (() -> Unit)? = null) {
|
||||
WarningCardMaterial3Style(
|
||||
content = {
|
||||
WarningBody(
|
||||
title = title,
|
||||
description = null,
|
||||
icon = icon,
|
||||
iconBackground = TangemTheme.colors.button.disabled,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* [WarningCard], but clickable (with an 'greater then' icon to the left)
|
||||
*
|
||||
|
|
@ -105,7 +128,8 @@ fun RefreshableWaringCard(
|
|||
@Composable
|
||||
private fun WarningBody(
|
||||
title: String,
|
||||
description: String,
|
||||
description: String?,
|
||||
iconBackground: Color = TangemTheme.colors.background.secondary,
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
additionalContent: @Composable () -> Unit = {},
|
||||
) {
|
||||
|
|
@ -119,6 +143,7 @@ private fun WarningBody(
|
|||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
iconBackground = iconBackground,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -136,6 +161,20 @@ private fun WarningCardSurface(onClick: (() -> Unit)? = null, content: @Composab
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
private fun WarningCardMaterial3Style(onClick: (() -> Unit)? = null, content: @Composable () -> Unit) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius16),
|
||||
backgroundColor = TangemTheme.colors.button.disabled,
|
||||
elevation = TangemTheme.dimens.elevation0,
|
||||
onClick = onClick ?: {},
|
||||
enabled = onClick != null,
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
// endregion elements
|
||||
|
||||
// region Preview
|
||||
|
|
|
|||
|
|
@ -3,14 +3,13 @@ package com.tangem.core.ui.components.appbar
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
|
|
@ -49,7 +48,11 @@ fun AppBarWithBackButton(
|
|||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size24)
|
||||
.clickable { onBackClick() },
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = false),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = onBackClick,
|
||||
),
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
if (!text.isNullOrBlank()) {
|
||||
|
|
|
|||
|
|
@ -4,10 +4,13 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -39,7 +42,10 @@ fun AppBarWithBackButtonAndIcon(
|
|||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(size = TangemTheme.dimens.size24)
|
||||
.clickable { onBackClick() },
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
) { onBackClick() },
|
||||
tint = TangemTheme.colors.icon.primary1,
|
||||
)
|
||||
AnimatedContent(
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package com.tangem.core.ui.components.atoms.text
|
||||
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
|
||||
internal class BoundCounter(
|
||||
private val text: String,
|
||||
private val textLayoutResult: TextLayoutResult,
|
||||
private val charPosition: (Int) -> Int,
|
||||
) {
|
||||
var string = ""
|
||||
private set
|
||||
var width = 0f
|
||||
private set
|
||||
|
||||
private var _nextCharWidth: Float? = null
|
||||
private var invalidCharsCount = 0
|
||||
|
||||
fun widthWithNextChar(): Float = width + nextCharWidth()
|
||||
|
||||
private fun nextCharWidth(): Float = _nextCharWidth ?: run {
|
||||
var boundingBox: Rect
|
||||
// invalidCharsCount fixes this bug: https://issuetracker.google.com/issues/197146630
|
||||
invalidCharsCount--
|
||||
do {
|
||||
boundingBox = textLayoutResult
|
||||
.getBoundingBox(charPosition(string.count() + ++invalidCharsCount))
|
||||
} while (boundingBox.right == 0f)
|
||||
_nextCharWidth = boundingBox.width
|
||||
boundingBox.width
|
||||
}
|
||||
|
||||
fun addNextChar() {
|
||||
string += text[charPosition(string.count())]
|
||||
width += nextCharWidth()
|
||||
_nextCharWidth = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,242 @@
|
|||
package com.tangem.core.ui.components.atoms.text
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.SubcomposeLayout
|
||||
import androidx.compose.ui.text.TextLayoutResult
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
sealed class TextEllipsis {
|
||||
|
||||
object Middle : TextEllipsis()
|
||||
|
||||
object End : TextEllipsis()
|
||||
|
||||
data class OffsetEnd(
|
||||
val offsetEnd: Int = 0,
|
||||
val hasSeparator: Boolean = true,
|
||||
) : TextEllipsis()
|
||||
}
|
||||
|
||||
/**
|
||||
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
|
||||
*
|
||||
* Customized Text with ellipsis. Ellipsis can be placed in: Middle, End or OffsetEnd (OffsetEnd with separator).
|
||||
*
|
||||
* * OffsetEnd can be useful to display big amounts with currency symbol. OffsetEnd 0 is equal to End.
|
||||
*/
|
||||
@Suppress("LongMethod")
|
||||
@Composable
|
||||
fun EllipsisText(
|
||||
text: String,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontStyle: FontStyle? = null,
|
||||
fontWeight: FontWeight? = null,
|
||||
fontFamily: FontFamily? = null,
|
||||
letterSpacing: TextUnit = TextUnit.Unspecified,
|
||||
textDecoration: TextDecoration? = null,
|
||||
textAlign: TextAlign? = null,
|
||||
lineHeight: TextUnit = TextUnit.Unspecified,
|
||||
softWrap: Boolean = true,
|
||||
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||
style: TextStyle = LocalTextStyle.current,
|
||||
ellipsis: TextEllipsis = TextEllipsis.End,
|
||||
) {
|
||||
val ellipsisText = remember(text) {
|
||||
if (ellipsis is TextEllipsis.OffsetEnd && ellipsis.hasSeparator) {
|
||||
ELLIPSIS_TEXT_WITH_SEPARATOR
|
||||
} else {
|
||||
ELLIPSIS_TEXT
|
||||
}
|
||||
}
|
||||
|
||||
// some letters, like "r", will have less width when placed right before "."
|
||||
// adding a space to prevent such case
|
||||
val layoutText = remember(text) { "$text $ellipsisText" }
|
||||
val textLayoutResultState = remember(layoutText) {
|
||||
mutableStateOf<TextLayoutResult?>(null)
|
||||
}
|
||||
SubcomposeLayout(modifier) { constraints ->
|
||||
// result is ignored - we only need to fill our textLayoutResult
|
||||
subcompose("measure") {
|
||||
Text(
|
||||
text = layoutText,
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
softWrap = softWrap,
|
||||
maxLines = 1,
|
||||
onTextLayout = { textLayoutResultState.value = it },
|
||||
style = style,
|
||||
)
|
||||
}.first().measure(Constraints())
|
||||
// to allow smart cast
|
||||
val textLayoutResult = textLayoutResultState.value
|
||||
?: // shouldn't happen - onTextLayout is called before subcompose finishes
|
||||
return@SubcomposeLayout layout(0, 0) {}
|
||||
val placeable = subcompose("visible") {
|
||||
val finalText = remember(text, textLayoutResult, constraints.maxWidth) {
|
||||
if (
|
||||
text.isEmpty() ||
|
||||
textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth
|
||||
) {
|
||||
// text not including ellipsis fits on the first line.
|
||||
return@remember text
|
||||
}
|
||||
|
||||
var ellipsisWidth = 0f
|
||||
layoutText.indices.toList()
|
||||
.takeLast(ellipsisText.length)
|
||||
.forEach widthLet@{
|
||||
ellipsisWidth += textLayoutResult.getBoundingBox(it).width
|
||||
}
|
||||
|
||||
val availableWidth = constraints.maxWidth - ellipsisWidth
|
||||
val startCounter = BoundCounter(text, textLayoutResult) { it }
|
||||
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
|
||||
|
||||
when (ellipsis) {
|
||||
TextEllipsis.Middle -> {
|
||||
middleEllipsisText(
|
||||
availableWidth,
|
||||
startCounter,
|
||||
endCounter,
|
||||
)
|
||||
}
|
||||
TextEllipsis.End -> {
|
||||
offsetEndEllipsisText(
|
||||
availableWidth = availableWidth,
|
||||
startCounter = startCounter,
|
||||
endCounter = endCounter,
|
||||
)
|
||||
}
|
||||
is TextEllipsis.OffsetEnd -> {
|
||||
offsetEndEllipsisText(
|
||||
availableWidth = availableWidth,
|
||||
startCounter = startCounter,
|
||||
endCounter = endCounter,
|
||||
offsetEnd = ellipsis.offsetEnd,
|
||||
withSeparator = ellipsis.hasSeparator,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = finalText,
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
fontStyle = fontStyle,
|
||||
fontWeight = fontWeight,
|
||||
fontFamily = fontFamily,
|
||||
letterSpacing = letterSpacing,
|
||||
textDecoration = textDecoration,
|
||||
textAlign = textAlign,
|
||||
lineHeight = lineHeight,
|
||||
softWrap = softWrap,
|
||||
onTextLayout = onTextLayout,
|
||||
style = style,
|
||||
)
|
||||
}[0].measure(constraints)
|
||||
layout(placeable.width, placeable.height) {
|
||||
placeable.place(0, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val ELLIPSIS_SEPARATOR = " "
|
||||
private const val ELLIPSIS_TEXT = "..."
|
||||
private const val ELLIPSIS_TEXT_WITH_SEPARATOR = ELLIPSIS_TEXT.plus(ELLIPSIS_SEPARATOR)
|
||||
|
||||
private fun middleEllipsisText(availableWidth: Float, startCounter: BoundCounter, endCounter: BoundCounter): String {
|
||||
while (availableWidth - startCounter.width - endCounter.width > 0) {
|
||||
val possibleEndWidth = endCounter.widthWithNextChar()
|
||||
if (
|
||||
startCounter.width >= possibleEndWidth &&
|
||||
availableWidth - startCounter.width - possibleEndWidth >= 0
|
||||
) {
|
||||
endCounter.addNextChar()
|
||||
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
|
||||
startCounter.addNextChar()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
return startCounter.string.trimEnd() + ELLIPSIS_TEXT + endCounter.string.reversed().trimStart()
|
||||
}
|
||||
|
||||
private fun offsetEndEllipsisText(
|
||||
availableWidth: Float,
|
||||
startCounter: BoundCounter,
|
||||
endCounter: BoundCounter,
|
||||
offsetEnd: Int = 0,
|
||||
withSeparator: Boolean = false,
|
||||
): String {
|
||||
while (availableWidth - startCounter.width - endCounter.width > 0) {
|
||||
val possibleEndWidth = endCounter.widthWithNextChar()
|
||||
if (
|
||||
offsetEnd > endCounter.string.length &&
|
||||
availableWidth - startCounter.width - possibleEndWidth >= 0
|
||||
) {
|
||||
endCounter.addNextChar()
|
||||
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
|
||||
startCounter.addNextChar()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
val ellipsis = if (withSeparator) ELLIPSIS_TEXT_WITH_SEPARATOR else ELLIPSIS_TEXT
|
||||
|
||||
return startCounter.string.trimEnd() + ellipsis + endCounter.string.reversed().trimStart()
|
||||
}
|
||||
|
||||
//region Preview
|
||||
@Preview(widthDp = 200)
|
||||
@Composable
|
||||
private fun EllipsisTexPreview(@PreviewParameter(EllipsisTexPreviewParameterProvider::class) ellipsis: TextEllipsis) {
|
||||
TangemTheme {
|
||||
EllipsisText(
|
||||
text = "11111111111111111111111111111111111111111111111111 END",
|
||||
ellipsis = ellipsis,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private class EllipsisTexPreviewParameterProvider : PreviewParameterProvider<TextEllipsis> {
|
||||
override val values: Sequence<TextEllipsis>
|
||||
get() = sequenceOf(
|
||||
TextEllipsis.Middle,
|
||||
TextEllipsis.End,
|
||||
TextEllipsis.OffsetEnd("TEXT".length),
|
||||
TextEllipsis.OffsetEnd("TEXT".length, false),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -6,6 +6,7 @@ import androidx.compose.material3.ModalBottomSheet
|
|||
import androidx.compose.material3.SheetState
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
|
@ -20,6 +21,7 @@ import kotlinx.coroutines.launch
|
|||
@Composable
|
||||
inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
||||
config: TangemBottomSheetConfig,
|
||||
contentColor: Color = TangemTheme.colors.background.primary,
|
||||
crossinline content: @Composable ColumnScope.(T) -> Unit,
|
||||
) {
|
||||
var isVisible by remember { mutableStateOf(value = config.isShow) }
|
||||
|
|
@ -29,9 +31,9 @@ inline fun <reified T : TangemBottomSheetConfigContent> TangemBottomSheet(
|
|||
ModalBottomSheet(
|
||||
onDismissRequest = config.onDismissRequest,
|
||||
sheetState = sheetState,
|
||||
containerColor = TangemTheme.colors.background.primary,
|
||||
containerColor = contentColor,
|
||||
shape = TangemTheme.shapes.bottomSheetLarge,
|
||||
dragHandle = { TangemBottomSheetDraggableHeader() },
|
||||
dragHandle = { TangemBottomSheetDraggableHeader(contentColor) },
|
||||
) {
|
||||
content(config.content)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package com.tangem.core.ui.components.fields
|
||||
package com.tangem.core.ui.components.fields.visualtransformations
|
||||
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
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.components.atoms.text.EllipsisText
|
||||
import com.tangem.core.ui.components.atoms.text.TextEllipsis
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIcon
|
||||
import com.tangem.core.ui.components.currency.tokenicon.TokenIconState
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* [Input Row Approx](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2207-810&mode=design&t=fM1ZU6zQF6g3CaTv-4)
|
||||
*
|
||||
* @param leftIcon left token state
|
||||
* @param leftTitle left token title
|
||||
* @param leftSubtitle left token subtitle
|
||||
* @param rightIcon right token state
|
||||
* @param rightTitle right token title
|
||||
* @param rightSubtitle right token subtitle
|
||||
* @param modifier composable modifier
|
||||
* @param showDivider show divider
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun InputRowApprox(
|
||||
leftIcon: TokenIconState,
|
||||
leftTitle: TextReference,
|
||||
leftSubtitle: TextReference,
|
||||
rightIcon: TokenIconState,
|
||||
rightTitle: TextReference,
|
||||
rightSubtitle: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
leftTitleEllipsisOffset: Int = 0,
|
||||
rightTitleEllipsisOffset: Int = 0,
|
||||
showDivider: Boolean = false,
|
||||
) {
|
||||
DividerContainer(
|
||||
showDivider = showDivider,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing12)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
InputRowApproxItem(
|
||||
iconState = leftIcon,
|
||||
title = leftTitle,
|
||||
subtitle = leftSubtitle,
|
||||
titleEllipsisOffset = leftTitleEllipsisOffset,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_approx_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
horizontal = TangemTheme.dimens.spacing4,
|
||||
vertical = TangemTheme.dimens.spacing10,
|
||||
),
|
||||
)
|
||||
InputRowApproxItem(
|
||||
iconState = rightIcon,
|
||||
title = rightTitle,
|
||||
subtitle = rightSubtitle,
|
||||
titleEllipsisOffset = rightTitleEllipsisOffset,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InputRowApproxItem(
|
||||
iconState: TokenIconState,
|
||||
title: TextReference,
|
||||
subtitle: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
titleEllipsisOffset: Int = 0,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier,
|
||||
) {
|
||||
TokenIcon(
|
||||
state = iconState,
|
||||
modifier = Modifier
|
||||
.size(TangemTheme.dimens.size36),
|
||||
)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
) {
|
||||
EllipsisText(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
ellipsis = TextEllipsis.OffsetEnd(titleEllipsisOffset),
|
||||
)
|
||||
EllipsisText(
|
||||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing2),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//region Preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InputRowApproxPreview_Light() {
|
||||
TangemTheme {
|
||||
InputRowApprox(
|
||||
leftIcon = TokenIconState.Loading,
|
||||
leftTitle = TextReference.Str("Left title USD"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle USD"),
|
||||
leftTitleEllipsisOffset = 3,
|
||||
rightIcon = TokenIconState.Loading,
|
||||
rightTitle = TextReference.Str("Right title Right title Right title Right title Right title USD"),
|
||||
rightSubtitle = TextReference.Str("Right subtitle Right subtitle Right subtitle USD"),
|
||||
rightTitleEllipsisOffset = 3,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InputRowApproxPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
InputRowApprox(
|
||||
leftIcon = TokenIconState.Loading,
|
||||
leftTitle = TextReference.Str("Left title Left title Left title Left title Left title USD"),
|
||||
leftSubtitle = TextReference.Str("Left subtitle Left subtitle Left subtitle USD"),
|
||||
leftTitleEllipsisOffset = 3,
|
||||
rightIcon = TokenIconState.Loading,
|
||||
rightTitle = TextReference.Str("Right title USD"),
|
||||
rightSubtitle = TextReference.Str("Right subtitle USD"),
|
||||
rightTitleEllipsisOffset = 3,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -0,0 +1,221 @@
|
|||
package com.tangem.core.ui.components.inputrow
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerWMax
|
||||
import com.tangem.core.ui.components.currency.tokenicon.LoadingIcon
|
||||
import com.tangem.core.ui.components.inputrow.inner.DividerContainer
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* [Input Row Best Rate](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-889&mode=dev)
|
||||
*
|
||||
* @param imageUrl image source url
|
||||
* @param title title
|
||||
* @param titleExtra title extra
|
||||
* @param subtitle subtitle
|
||||
* @param modifier composable modifier
|
||||
* @param showTag show tag
|
||||
* @param showDivider show divider
|
||||
* @param onIconClick icon click
|
||||
*/
|
||||
@Composable
|
||||
fun InputRowBestRate(
|
||||
imageUrl: String,
|
||||
title: TextReference,
|
||||
titleExtra: TextReference,
|
||||
subtitle: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
showTag: Boolean = false,
|
||||
showDivider: Boolean = false,
|
||||
onIconClick: (() -> Unit)? = null,
|
||||
) {
|
||||
DividerContainer(
|
||||
showDivider = showDivider,
|
||||
modifier = modifier,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
InnerIcon(imageUrl = imageUrl)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing12),
|
||||
) {
|
||||
InnerTitle(
|
||||
title = title,
|
||||
titleExtra = titleExtra,
|
||||
showTag = showTag,
|
||||
)
|
||||
Text(
|
||||
text = subtitle.resolveReference(),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier.padding(top = TangemTheme.dimens.spacing8),
|
||||
)
|
||||
}
|
||||
SpacerWMax()
|
||||
onIconClick?.let {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
modifier = Modifier
|
||||
.padding(vertical = TangemTheme.dimens.spacing10)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
onClick = onIconClick,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InnerTitle(title: TextReference, titleExtra: TextReference, showTag: Boolean = false) {
|
||||
Row {
|
||||
Text(
|
||||
text = title.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = titleExtra.resolveReference(),
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
if (showTag) {
|
||||
Text(
|
||||
text = stringResource(R.string.express_provider_best_rate),
|
||||
style = TangemTheme.typography.caption1,
|
||||
color = TangemTheme.colors.icon.accent,
|
||||
modifier = Modifier
|
||||
.padding(start = TangemTheme.dimens.spacing4)
|
||||
.background(
|
||||
color = TangemTheme.colors.icon.accent.copy(alpha = 0.1f),
|
||||
shape = RoundedCornerShape(TangemTheme.dimens.radius20),
|
||||
)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing6),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InnerIcon(imageUrl: String) {
|
||||
SubcomposeAsyncImage(
|
||||
modifier = Modifier.size(TangemTheme.dimens.size40),
|
||||
model = ImageRequest.Builder(context = LocalContext.current)
|
||||
.data(imageUrl)
|
||||
.crossfade(enable = true)
|
||||
.allowHardware(enable = false)
|
||||
.build(),
|
||||
loading = { LoadingIcon() },
|
||||
error = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = TangemTheme.colors.background.tertiary,
|
||||
shape = CircleShape,
|
||||
),
|
||||
)
|
||||
},
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
//region preview
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InputRowBestRatePreview_Light(
|
||||
@PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData,
|
||||
) {
|
||||
TangemTheme {
|
||||
InputRowBestRate(
|
||||
imageUrl = "",
|
||||
title = data.title,
|
||||
titleExtra = data.titleExtra,
|
||||
subtitle = data.subtitle,
|
||||
showTag = data.showTag,
|
||||
onIconClick = data.iconClick,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun InputRowBestRatePreview_Dark(
|
||||
@PreviewParameter(InputRowBestRatePreviewDataProvider::class) data: InputRowBestRatePreviewData,
|
||||
) {
|
||||
TangemTheme(isDark = true) {
|
||||
InputRowBestRate(
|
||||
imageUrl = "",
|
||||
title = data.title,
|
||||
titleExtra = data.titleExtra,
|
||||
subtitle = data.subtitle,
|
||||
showTag = data.showTag,
|
||||
onIconClick = data.iconClick,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.action),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private data class InputRowBestRatePreviewData(
|
||||
val title: TextReference,
|
||||
val titleExtra: TextReference,
|
||||
val showTag: Boolean,
|
||||
val subtitle: TextReference,
|
||||
val iconClick: (() -> Unit)?,
|
||||
)
|
||||
|
||||
private class InputRowBestRatePreviewDataProvider : PreviewParameterProvider<InputRowBestRatePreviewData> {
|
||||
override val values: Sequence<InputRowBestRatePreviewData>
|
||||
get() = sequenceOf(
|
||||
InputRowBestRatePreviewData(
|
||||
title = TextReference.Str("1inch"),
|
||||
titleExtra = TextReference.Str("DEX"),
|
||||
subtitle = TextReference.Str("0,64554846 DAI ≈ 1 MATIC "),
|
||||
showTag = true,
|
||||
iconClick = {},
|
||||
),
|
||||
InputRowBestRatePreviewData(
|
||||
title = TextReference.Str("ChangeNow"),
|
||||
titleExtra = TextReference.Str("CEX"),
|
||||
subtitle = TextReference.Str("0,64554846 DAI ≈ 1 MATIC "),
|
||||
showTag = false,
|
||||
iconClick = null,
|
||||
),
|
||||
)
|
||||
}
|
||||
//endregion
|
||||
|
|
@ -8,7 +8,9 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -42,8 +44,18 @@ import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsSta
|
|||
* >Figma component</a>
|
||||
*/
|
||||
@Composable
|
||||
fun Notification(config: NotificationConfig, modifier: Modifier = Modifier, iconTint: Color? = null) {
|
||||
BaseContainer(buttonsState = config.buttonsState, onClick = config.onClick, modifier = modifier) {
|
||||
fun Notification(
|
||||
config: NotificationConfig,
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color? = null,
|
||||
iconTint: Color? = null,
|
||||
) {
|
||||
BaseContainer(
|
||||
buttonsState = config.buttonsState,
|
||||
onClick = config.onClick,
|
||||
modifier = modifier,
|
||||
containerColor = containerColor,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(all = TangemTheme.dimens.spacing12),
|
||||
verticalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens.spacing12),
|
||||
|
|
@ -71,9 +83,10 @@ private fun BaseContainer(
|
|||
buttonsState: NotificationConfig.ButtonsState?,
|
||||
onClick: (() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color? = null,
|
||||
content: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
val containerColor by rememberUpdatedState(
|
||||
val tempContainerColor by rememberUpdatedState(
|
||||
newValue = if (buttonsState != null || onClick != null) {
|
||||
TangemTheme.colors.background.primary
|
||||
} else {
|
||||
|
|
@ -88,7 +101,7 @@ private fun BaseContainer(
|
|||
.fillMaxWidth(),
|
||||
enabled = onClick != null,
|
||||
shape = TangemTheme.shapes.roundedCornersXMedium,
|
||||
color = containerColor,
|
||||
color = containerColor ?: tempContainerColor,
|
||||
) {
|
||||
Box(content = content)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package com.tangem.core.ui.components.rows
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH28
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Simple clickable action row, without input and icon
|
||||
*
|
||||
* https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=Ygv5sohTTHYAQcBS-4
|
||||
*/
|
||||
@Composable
|
||||
fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier, isClickable: Boolean = true) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(color = TangemTheme.colors.background.action)
|
||||
.height(TangemTheme.dimens.size44)
|
||||
.fillMaxWidth(),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(end = TangemTheme.dimens.spacing48)
|
||||
.align(Alignment.CenterStart),
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
|
||||
) {
|
||||
AnimatedContent(targetState = title, label = "") {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
AnimatedContent(targetState = description, label = "") {
|
||||
Text(
|
||||
text = it,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (isClickable) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.ic_chevron_right_24),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(alignment = Alignment.CenterEnd)
|
||||
.padding(end = TangemTheme.dimens.spacing12),
|
||||
tint = TangemTheme.colors.icon.informative,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SimpleActionRowPreview() {
|
||||
Column {
|
||||
TangemTheme(isDark = false) {
|
||||
SimpleActionRow(
|
||||
title = "Title",
|
||||
description = "Description",
|
||||
)
|
||||
}
|
||||
|
||||
SpacerH28()
|
||||
|
||||
TangemTheme(isDark = false) {
|
||||
SimpleActionRow(
|
||||
title = "Title",
|
||||
description = "Description",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,160 @@
|
|||
package com.tangem.core.ui.components.rows
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.text.TextStyle
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
@Composable
|
||||
fun SelectorRowItem(
|
||||
@StringRes titleRes: Int,
|
||||
@DrawableRes iconRes: Int,
|
||||
onSelect: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
preEllipsize: TextReference? = null,
|
||||
postEllipsize: TextReference? = null,
|
||||
isSelected: Boolean = false,
|
||||
showDivider: Boolean = true,
|
||||
) {
|
||||
val iconTint by animateColorAsState(
|
||||
targetValue = if (isSelected) {
|
||||
TangemTheme.colors.icon.accent
|
||||
} else {
|
||||
TangemTheme.colors.icon.informative
|
||||
},
|
||||
label = "Selector icon tint change",
|
||||
)
|
||||
|
||||
val textStyle = if (isSelected) {
|
||||
TangemTheme.typography.subtitle2
|
||||
} else {
|
||||
TangemTheme.typography.body2
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onSelect() },
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Icon(
|
||||
painter = painterResource(iconRes),
|
||||
tint = iconTint,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing12,
|
||||
bottom = TangemTheme.dimens.spacing12,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing8,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
if (preEllipsize != null && postEllipsize != null) {
|
||||
SelectorValueContent(
|
||||
amount = preEllipsize,
|
||||
symbol = postEllipsize,
|
||||
textStyle = textStyle,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showDivider) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(TangemTheme.dimens.size1)
|
||||
.padding(horizontal = TangemTheme.dimens.spacing12)
|
||||
.background(TangemTheme.colors.stroke.primary)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RowScope.SelectorValueContent(amount: TextReference, symbol: TextReference, textStyle: TextStyle) {
|
||||
Text(
|
||||
text = amount.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
textAlign = TextAlign.End,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
maxLines = 1,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing4,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
Text(
|
||||
text = symbol.resolveReference(),
|
||||
style = textStyle,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = TangemTheme.dimens.spacing1,
|
||||
end = TangemTheme.dimens.spacing12,
|
||||
top = TangemTheme.dimens.spacing14,
|
||||
bottom = TangemTheme.dimens.spacing14,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SelectorRowItemPreview_Light() {
|
||||
TangemTheme {
|
||||
SelectorRowItem(
|
||||
titleRes = R.string.common_fee_selector_option_slow,
|
||||
iconRes = R.drawable.ic_tortoise_24,
|
||||
preEllipsize = TextReference.Str("1000"),
|
||||
postEllipsize = TextReference.Str("$"),
|
||||
isSelected = true,
|
||||
onSelect = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun SelectorRowItemPreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
SelectorRowItem(
|
||||
titleRes = R.string.common_fee_selector_option_slow,
|
||||
iconRes = R.drawable.ic_tortoise_24,
|
||||
preEllipsize = TextReference.Str("1000"),
|
||||
postEllipsize = TextReference.Str("$"),
|
||||
isSelected = true,
|
||||
onSelect = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
package com.tangem.core.ui.components.transactions
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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 com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.utils.toDateFormat
|
||||
import com.tangem.core.ui.utils.toTimeFormat
|
||||
|
||||
/**
|
||||
* Common transaction done screen title
|
||||
*
|
||||
* @param titleRes title resource
|
||||
* @param date transaction timestamp in millis
|
||||
*/
|
||||
@Composable
|
||||
fun TransactionDoneTitle(@StringRes titleRes: Int, date: Long, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_empty_in_process_64),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing8)
|
||||
.size(TangemTheme.dimens.size64),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = titleRes),
|
||||
style = TangemTheme.typography.h3,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing32),
|
||||
)
|
||||
Text(
|
||||
text = stringResource(id = R.string.send_date_format, date.toDateFormat(), date.toTimeFormat()),
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.tertiary,
|
||||
modifier = Modifier
|
||||
.padding(top = TangemTheme.dimens.spacing4),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// region Previews
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransactionDoneTitlePreview_Light() {
|
||||
TangemTheme {
|
||||
TransactionDoneTitle(
|
||||
titleRes = R.string.sent_transaction_sent_title,
|
||||
date = 0,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun TransactionDoneTitlePreview_Dark() {
|
||||
TangemTheme(isDark = true) {
|
||||
TransactionDoneTitle(
|
||||
titleRes = R.string.sent_transaction_sent_title,
|
||||
date = 0,
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors.background.tertiary)
|
||||
.padding(TangemTheme.dimens.spacing16),
|
||||
)
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
|
@ -7,18 +7,34 @@ import androidx.compose.ui.composed
|
|||
import androidx.compose.ui.draw.clip
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modifier = composed {
|
||||
val modifierWithHorizontalPadding = this.padding(horizontal = TangemTheme.dimens.spacing16)
|
||||
fun Modifier.roundedShapeItemDecoration(
|
||||
currentIndex: Int,
|
||||
lastIndex: Int,
|
||||
addDefaultPadding: Boolean = true,
|
||||
): Modifier = composed {
|
||||
val modifier = if (addDefaultPadding) this.padding(horizontal = TangemTheme.dimens.spacing16) else this
|
||||
val isSingleItem = currentIndex == 0 && lastIndex == 0
|
||||
when {
|
||||
isSingleItem -> {
|
||||
modifierWithHorizontalPadding
|
||||
.padding(top = TangemTheme.dimens.spacing14)
|
||||
modifier
|
||||
.then(
|
||||
if (addDefaultPadding) {
|
||||
Modifier.padding(top = TangemTheme.dimens.spacing14)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.clip(shape = TangemTheme.shapes.roundedCornersXMedium)
|
||||
}
|
||||
currentIndex == 0 -> {
|
||||
modifierWithHorizontalPadding
|
||||
.padding(top = TangemTheme.dimens.spacing14)
|
||||
modifier
|
||||
.then(
|
||||
if (addDefaultPadding) {
|
||||
Modifier.padding(top = TangemTheme.dimens.spacing14)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.clip(
|
||||
shape = RoundedCornerShape(
|
||||
topStart = TangemTheme.dimens.radius16,
|
||||
|
|
@ -27,7 +43,7 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi
|
|||
)
|
||||
}
|
||||
currentIndex == lastIndex -> {
|
||||
modifierWithHorizontalPadding
|
||||
modifier
|
||||
.clip(
|
||||
shape = RoundedCornerShape(
|
||||
bottomStart = TangemTheme.dimens.radius16,
|
||||
|
|
@ -35,6 +51,6 @@ fun Modifier.roundedShapeItemDecoration(currentIndex: Int, lastIndex: Int): Modi
|
|||
),
|
||||
)
|
||||
}
|
||||
else -> modifierWithHorizontalPadding
|
||||
else -> modifier
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.ui.event
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.NonRestartableComposable
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
/**
|
||||
* A Composable function that reacts to a given [StateEvent], executing the provided action only once when the event
|
||||
|
|
@ -17,8 +18,8 @@ import androidx.compose.runtime.NonRestartableComposable
|
|||
fun <A> EventEffect(event: StateEvent<A>, onTrigger: suspend (data: A) -> Unit) {
|
||||
LaunchedEffect(event) {
|
||||
if (event is StateEvent.Triggered<A>) {
|
||||
onTrigger(event.data)
|
||||
event.onConsume()
|
||||
launch { onTrigger(event.data) }
|
||||
.invokeOnCompletion { event.onConsume() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,16 @@ fun CryptoCurrency.Token.tryGetBackgroundForTokenIcon(
|
|||
): Color {
|
||||
if (isGrayscale) return TangemColorPalette.Dark2
|
||||
|
||||
return tryGetBackgroundForTokenIcon(contractAddress = contractAddress, fallbackColor = fallbackColor)
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to extract a background color from the contract address of a token.
|
||||
*
|
||||
* @param fallbackColor The color to use as a fallback.
|
||||
* @return The extracted background color or the fallback color if extraction fails or if it is a test network token.
|
||||
*/
|
||||
fun tryGetBackgroundForTokenIcon(contractAddress: String, fallbackColor: Color = TangemColorPalette.Black): Color {
|
||||
return try {
|
||||
val colorHex = "#" + contractAddress.substring(range = COLOR_HEX_START_INDEX..COLOR_HEX_END_INDEX)
|
||||
Color(colorHex.toColorInt())
|
||||
|
|
|
|||
|
|
@ -104,6 +104,16 @@ fun combinedReference(refs: WrappedList<TextReference>): TextReference {
|
|||
return TextReference.Combined(refs)
|
||||
}
|
||||
|
||||
/**
|
||||
* Combines multiple [TextReference] instances into a single [TextReference].
|
||||
*
|
||||
* @param refs Vararg of [TextReference] instances to be combined.
|
||||
* @return A [TextReference] representing the combined text references.
|
||||
*/
|
||||
fun combinedReference(vararg refs: TextReference): TextReference {
|
||||
return TextReference.Combined(WrappedList(listOf(*refs)))
|
||||
}
|
||||
|
||||
/** Resolve [TextReference] as [String] */
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ data class TangemDimens internal constructor(
|
|||
val size0: Dp = 0.dp,
|
||||
val size0_5: Dp = 0.5.dp,
|
||||
val size1: Dp = 1.dp,
|
||||
val size1_5: Dp = 1.5.dp,
|
||||
val size2: Dp = 2.dp,
|
||||
val size4: Dp = 4.dp,
|
||||
val size5: Dp = 5.dp,
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ private fun darkThemeColors(): TangemColors {
|
|||
),
|
||||
button = TangemColors.Button(
|
||||
primary = TangemColorPalette.Light4,
|
||||
secondary = TangemColorPalette.Dark5,
|
||||
secondary = TangemColorPalette.Dark4,
|
||||
disabled = TangemColorPalette.Dark5,
|
||||
positiveDisabled = TangemColorPalette.DarkGreen,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.tangem.core.ui.utils
|
||||
|
||||
import com.tangem.domain.tokens.model.CryptoCurrency
|
||||
import java.math.BigDecimal
|
||||
import java.math.RoundingMode
|
||||
import java.text.NumberFormat
|
||||
|
|
@ -24,6 +25,10 @@ object BigDecimalFormatter {
|
|||
return formatter.format(cryptoAmount) + "\u2009$cryptoCurrency"
|
||||
}
|
||||
|
||||
fun formatCryptoAmount(cryptoAmount: BigDecimal?, cryptoCurrency: CryptoCurrency): String {
|
||||
return formatCryptoAmount(cryptoAmount, cryptoCurrency.symbol, cryptoCurrency.decimals)
|
||||
}
|
||||
|
||||
fun formatFiatAmount(
|
||||
fiatAmount: BigDecimal?,
|
||||
fiatCurrencyCode: String,
|
||||
|
|
|
|||
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