Updated on 2026-08-14

This commit is contained in:
Tangem 2023-10-30 17:26:52 +03:00
commit b02ec06761
30 changed files with 222 additions and 92 deletions

View file

@ -7,6 +7,7 @@ import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.userwallets.UserWalletIdBuilder
import com.tangem.tap.common.extensions.stripZeroPlainString
import java.util.concurrent.CopyOnWriteArrayList
class AdditionalFeedbackInfo {
@ -36,7 +37,7 @@ class AdditionalFeedbackInfo {
var userWalletId: String = ""
// wallets
val walletsInfo = mutableListOf<EmailWalletInfo>()
val walletsInfo = CopyOnWriteArrayList<EmailWalletInfo>()
var onSendErrorWalletInfo: EmailWalletInfo? = null
private set
var signedHashesCount: String = ""
@ -74,9 +75,7 @@ class AdditionalFeedbackInfo {
@Deprecated("Don't use it directly")
fun setWalletsInfo(walletManagers: List<WalletManager>) {
walletsInfo.clear()
walletManagers.forEach {
walletsInfo.add(createEmailWalletInfo(it))
}
walletsInfo.addAll(elements = walletManagers.map(::createEmailWalletInfo))
}
fun updateOnSendError(

View file

@ -188,8 +188,9 @@ private fun handleAction(action: Action, appState: () -> AppState?) {
walletManagersFacade
.getAll(userWallet.walletId)
.distinctUntilChanged()
.onEach(infoHolder::setWalletsInfo)
.launchIn(scope)
.launchIn(mainScope)
}
}
.flowOn(Dispatchers.IO)

View file

@ -108,9 +108,9 @@ class UserTokensRepository(
.fold(
onSuccess = { response ->
response.getOrThrow()
.also { storageService.saveUserTokens(userWalletId, it) }
.tokens
.mapNotNull(Currency.Companion::fromTokenResponse)
.also { storageService.saveUserTokens(userWalletId, it.toUserTokensResponse()) }
.distinct()
},
onFailure = { handleGetUserTokensFailure(userWalletId = userWalletId, error = it) },

View file

@ -74,7 +74,7 @@ internal fun TokensListScreen(stateHolder: TokensListStateHolder, modifier: Modi
val tokens = stateHolder.tokens.collectAsLazyPagingItems()
TokensListContent(
isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible && !stateHolder.isLoading,
isDifferentAddressesBlockVisible = stateHolder.isDifferentAddressesBlockVisible,
tokens = tokens,
scaffoldPadding = scaffoldPadding,
bottomMarginDp = floatingButtonHeight,
@ -125,8 +125,11 @@ private fun TokensListContent(
state = state,
contentPadding = PaddingValues(bottom = bottomMarginDp),
) {
if (isDifferentAddressesBlockVisible) {
item { DifferentAddressesWarning() }
item(
key = "DifferentAddressesWarning$isDifferentAddressesBlockVisible",
contentType = "DifferentAddressesWarning$isDifferentAddressesBlockVisible",
) {
if (isDifferentAddressesBlockVisible) DifferentAddressesWarning()
}
tokens.itemKey(TokenItemState::composedId)

View file

@ -111,8 +111,8 @@
<FrameLayout
android:id="@+id/imv_activation_success"
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:visibility="invisible"
app:layout_constraintBottom_toBottomOf="@+id/imv_card_background"
@ -121,23 +121,23 @@
app:layout_constraintTop_toBottomOf="@+id/imv_card_background">
<View
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@drawable/shape_success_circle"
android:backgroundTint="@color/background_primary" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_success_middle"
android:layout_width="55dp"
android:layout_height="55dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:background="@android:color/transparent"
android:src="@drawable/img_onboarding_success" />
<View
android:layout_width="60dp"
android:layout_height="60dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:alpha="0.2"
android:background="@drawable/shape_success_circle"

View file

@ -77,6 +77,7 @@ fun PrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
showProgress: Boolean = false,
enabled: Boolean = true,
) {
@ -88,6 +89,7 @@ fun PrimaryButton(
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = enabled,
showProgress = showProgress,
size = size,
)
}
@ -100,6 +102,7 @@ fun PrimaryButtonIconEnd(
@DrawableRes iconResId: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
showProgress: Boolean = false,
enabled: Boolean = true,
) {
@ -111,6 +114,7 @@ fun PrimaryButtonIconEnd(
colors = TangemButtonsDefaults.primaryButtonColors,
enabled = enabled,
showProgress = showProgress,
size = size,
)
}
@ -144,6 +148,7 @@ fun SecondaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
size: TangemButtonSize = TangemButtonSize.Default,
showProgress: Boolean = false,
enabled: Boolean = true,
) {
@ -155,6 +160,7 @@ fun SecondaryButton(
colors = TangemButtonsDefaults.secondaryButtonColors,
enabled = enabled,
showProgress = showProgress,
size = size,
)
}

View file

@ -14,6 +14,7 @@ enum class TangemButtonSize {
Selector,
Action,
RoundedAction,
WideAction,
}
@Composable
@ -25,12 +26,15 @@ internal fun TangemButtonSize.toHeightDp(): Dp = when (this) {
TangemButtonSize.Action,
TangemButtonSize.RoundedAction,
-> TangemTheme.dimens.size36
TangemButtonSize.WideAction -> TangemTheme.dimens.size40
}
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toShape(): Shape = when (this) {
TangemButtonSize.Default -> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.Default,
TangemButtonSize.WideAction,
-> TangemTheme.shapes.roundedCornersMedium
TangemButtonSize.Text -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Selector -> TangemTheme.shapes.roundedCornersSmall
TangemButtonSize.Action -> TangemTheme.shapes.roundedCornersMedium
@ -40,7 +44,9 @@ internal fun TangemButtonSize.toShape(): Shape = when (this) {
@Composable
@ReadOnlyComposable
internal fun TangemButtonSize.toIconPadding(): Dp = when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing4
TangemButtonSize.Default,
TangemButtonSize.WideAction,
-> TangemTheme.dimens.spacing4
TangemButtonSize.Text -> TangemTheme.dimens.spacing8
TangemButtonSize.Selector -> 0.dp
TangemButtonSize.Action,
@ -80,6 +86,12 @@ internal fun TangemButtonSize.toContentPadding(icon: TangemButtonIconPosition):
start = horizontalPadding.first,
end = horizontalPadding.second,
)
TangemButtonSize.WideAction -> PaddingValues(
top = TangemTheme.dimens.spacing10,
bottom = TangemTheme.dimens.spacing10,
start = horizontalPadding.first,
end = horizontalPadding.second,
)
}
}
@ -87,7 +99,9 @@ internal fun TangemButtonSize.toContentPadding(icon: TangemButtonIconPosition):
@ReadOnlyComposable
internal fun TangemButtonSize.toHorizontalContentPadding(icon: TangemButtonIconPosition): Pair<Dp, Dp> {
return when (this) {
TangemButtonSize.Default -> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32
TangemButtonSize.Default,
TangemButtonSize.WideAction,
-> TangemTheme.dimens.spacing32 to TangemTheme.dimens.spacing32
TangemButtonSize.Text -> when (icon) {
is TangemButtonIconPosition.None -> TangemTheme.dimens.spacing16 to TangemTheme.dimens.spacing16
is TangemButtonIconPosition.Start -> TangemTheme.dimens.spacing14 to TangemTheme.dimens.spacing16

View file

@ -24,6 +24,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
@ -178,6 +179,7 @@ private fun SingleSecondaryButton(config: NotificationButtonsState.SecondaryButt
text = config.text.resolveReference(),
onClick = config.onClick,
modifier = Modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction,
)
}
@ -189,12 +191,14 @@ private fun SinglePrimaryButton(config: NotificationButtonsState.PrimaryButtonCo
iconResId = config.iconResId,
onClick = config.onClick,
modifier = Modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction,
)
} else {
PrimaryButton(
text = config.text.resolveReference(),
onClick = config.onClick,
modifier = Modifier.fillMaxWidth(),
size = TangemButtonSize.WideAction,
)
}
}
@ -206,12 +210,14 @@ private fun PairButtons(config: NotificationButtonsState.PairButtonsConfig) {
text = config.secondaryText.resolveReference(),
onClick = config.onSecondaryClick,
modifier = Modifier.weight(weight = 1f),
size = TangemButtonSize.WideAction,
)
PrimaryButton(
text = config.primaryText.resolveReference(),
onClick = config.onPrimaryClick,
modifier = Modifier.weight(weight = 1f),
size = TangemButtonSize.WideAction,
)
}
}

View file

@ -78,7 +78,7 @@ data class TangemTypography internal constructor(
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
),
val caption1: TextStyle = TextStyle(
fontFamily = RobotoFamily,

View file

@ -135,6 +135,7 @@ class DefaultWalletManagersFacade(
userWalletId: UserWalletId,
network: Network,
addressType: AddressType,
contractAddress: String?,
): String {
val blockchain = Blockchain.fromId(network.id.value)
@ -148,8 +149,12 @@ class DefaultWalletManagersFacade(
"Unable to get a wallet manager for blockchain: $blockchain"
}
val address = walletManager.wallet.addresses.find { it.type == addressType }?.value
return walletManager.wallet.getExploreUrl(address)
val address = walletManager
.wallet
.addresses
.find { it.type == addressType }
?.value ?: walletManager.wallet.address
return blockchain.getExploreUrl(address, contractAddress)
}
override suspend fun getTxHistoryState(userWalletId: UserWalletId, network: Network): TxHistoryState {

View file

@ -62,10 +62,16 @@ interface WalletManagersFacade {
* @param userWalletId The ID of the user's wallet.
* @param network The network.
* @param addressType Address type.
* @param contractAddress Contract address if currency is Token.
*
* @return The network explorer URL, maybe empty if the wallet manager was not found.
* */
suspend fun getExploreUrl(userWalletId: UserWalletId, network: Network, addressType: AddressType): String
suspend fun getExploreUrl(
userWalletId: UserWalletId,
network: Network,
addressType: AddressType,
contractAddress: String?,
): String
/**
* Returns transactions count

View file

@ -75,13 +75,13 @@ data class CryptoCurrencyStatus(
*/
data class NoAccount(
val amountToCreateAccount: BigDecimal,
override val fiatAmount: BigDecimal?,
override val priceChange: BigDecimal?,
override val fiatRate: BigDecimal?,
override val networkAddress: NetworkAddress?,
) : Status(isError = false) {
override val amount: BigDecimal = BigDecimal.ZERO
override val fiatAmount: BigDecimal = BigDecimal.ZERO
}
/**

View file

@ -31,6 +31,7 @@ internal class CurrencyStatusOperations(
private fun createNoAccountStatus(status: NetworkStatus.NoAccount): CryptoCurrencyStatus.NoAccount =
CryptoCurrencyStatus.NoAccount(
amountToCreateAccount = status.amountToCreateAccount,
fiatAmount = if (quote == null) null else BigDecimal.ZERO,
priceChange = quote?.priceChange,
fiatRate = quote?.fiatRate,
networkAddress = status.address,

View file

@ -60,6 +60,7 @@ internal object MockTokensStates {
val tokenState7 = CryptoCurrencyStatus(
currency = MockTokens.token7,
value = CryptoCurrencyStatus.NoAccount(
fiatAmount = BigDecimal.ZERO,
priceChange = MockQuotes.quote7.priceChange,
fiatRate = MockQuotes.quote7.fiatRate,
amountToCreateAccount = MockNetworks.amountToCreateAccount,
@ -70,6 +71,7 @@ internal object MockTokensStates {
val tokenState8 = CryptoCurrencyStatus(
currency = MockTokens.token8,
value = CryptoCurrencyStatus.NoAccount(
fiatAmount = BigDecimal.ZERO,
priceChange = MockQuotes.quote8.priceChange,
fiatRate = MockQuotes.quote8.fiatRate,
amountToCreateAccount = MockNetworks.amountToCreateAccount,
@ -80,6 +82,7 @@ internal object MockTokensStates {
val tokenState9 = CryptoCurrencyStatus(
currency = MockTokens.token9,
value = CryptoCurrencyStatus.NoAccount(
fiatAmount = BigDecimal.ZERO,
priceChange = MockQuotes.quote9.priceChange,
fiatRate = MockQuotes.quote9.fiatRate,
amountToCreateAccount = MockNetworks.amountToCreateAccount,
@ -90,6 +93,7 @@ internal object MockTokensStates {
val tokenState10 = CryptoCurrencyStatus(
currency = MockTokens.token10,
value = CryptoCurrencyStatus.NoAccount(
fiatAmount = BigDecimal.ZERO,
priceChange = MockQuotes.quote10.priceChange,
fiatRate = MockQuotes.quote10.fiatRate,
amountToCreateAccount = MockNetworks.amountToCreateAccount,

View file

@ -1,8 +1,8 @@
package com.tangem.domain.wallets.usecase
import arrow.core.raise.catch
import com.tangem.domain.tokens.model.Network
import com.tangem.blockchain.common.address.AddressType
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.walletmanager.WalletManagersFacade
import com.tangem.domain.wallets.models.UserWalletId
@ -12,11 +12,19 @@ class GetExploreUrlUseCase(private val walletsManagersFacade: WalletManagersFaca
// FIXME: Handle error
suspend operator fun invoke(
userWalletId: UserWalletId,
network: Network,
currency: CryptoCurrency,
addressType: AddressType = AddressType.Default,
): String {
return catch({ walletsManagersFacade.getExploreUrl(userWalletId, network, addressType) }) {
""
}
return catch(
block = {
walletsManagersFacade.getExploreUrl(
userWalletId = userWalletId,
network = currency.network,
addressType = addressType,
contractAddress = (currency as? CryptoCurrency.Token)?.contractAddress,
)
},
catch = { "" },
)
}
}

View file

@ -55,6 +55,7 @@ interface SwapInteractor {
* @param fromToken [Currency] from which want to swap
* @param toToken [Currency] that receive after swap
* @param amountToSwap amount you want to swap
* @param selectedFee selected fee to swap
* @return
*/
@Throws(IllegalStateException::class)
@ -63,6 +64,7 @@ interface SwapInteractor {
fromToken: Currency,
toToken: Currency,
amountToSwap: String,
selectedFee: FeeType = FeeType.NORMAL,
): SwapState
/**

View file

@ -167,6 +167,7 @@ internal class SwapInteractorImpl @Inject constructor(
fromToken: Currency,
toToken: Currency,
amountToSwap: String,
selectedFee: FeeType,
): SwapState {
syncWalletBalanceForTokens(networkId, listOf(fromToken, toToken))
val amountDecimal = toBigDecimalOrNull(amountToSwap)
@ -190,6 +191,7 @@ internal class SwapInteractorImpl @Inject constructor(
fromToken = fromToken,
toToken = toToken,
amount = amount,
selectedFee = selectedFee,
)
} else {
loadQuoteData(
@ -454,6 +456,7 @@ internal class SwapInteractorImpl @Inject constructor(
fromToken: Currency,
toToken: Currency,
amount: SwapAmount,
selectedFee: FeeType,
): SwapState {
repository.prepareSwapTransaction(
networkId = networkId,
@ -475,10 +478,14 @@ internal class SwapInteractorImpl @Inject constructor(
derivationPath = derivationPath,
)
val txFeeState = proxyFeesToFeeState(networkId, feeData)
val feeByPriority = when (selectedFee) {
FeeType.NORMAL -> txFeeState.normalFee.feeValue
FeeType.PRIORITY -> txFeeState.priorityFee.feeValue
}
val isBalanceIncludeFeeEnough =
isBalanceEnough(networkId, fromToken, amount, txFeeState.priorityFee.feeValue)
isBalanceEnough(networkId, fromToken, amount, feeByPriority)
val isFeeEnough = checkFeeIsEnough(
fee = txFeeState.normalFee.feeValue,
fee = feeByPriority,
spendAmount = amount,
networkId = networkId,
fromToken = fromToken,

View file

@ -312,7 +312,7 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider:
} else {
permissionState
}
return when (val fee = uiState.fee) {
val updateState = when (val fee = uiState.fee) {
is FeeState.Loaded -> {
getUpdatedFeeStateForEnoughFee(uiState, fee, item, newSelectedItem, newPermissionState, isFeeEnough)
}
@ -321,6 +321,15 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider:
}
else -> uiState
}
return if (isFeeEnough) {
updateState.copy(
warnings = uiState.warnings.filterNot { it is SwapWarning.InsufficientFunds },
)
} else {
updateState.copy(
warnings = uiState.warnings.plus(SwapWarning.InsufficientFunds),
)
}
}
@Suppress("LongParameterList")
@ -348,6 +357,9 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider:
return uiState.copy(
fee = newFeeState,
permissionState = newPermissionState,
swapButton = uiState.swapButton.copy(
enabled = isFeeEnough,
),
)
}
@ -376,6 +388,9 @@ internal class StateBuilder(val actions: UiActions, val isBalanceHiddenProvider:
return uiState.copy(
fee = newFeeState,
permissionState = newPermissionState,
swapButton = uiState.swapButton.copy(
enabled = isFeeEnough,
),
)
}

View file

@ -205,6 +205,7 @@ internal class SwapViewModel @Inject constructor(
fromToken = fromToken,
toToken = toToken,
amountToSwap = amount,
selectedFee = dataState.selectedFee?.feeType ?: FeeType.NORMAL,
)
}
},

View file

@ -90,23 +90,33 @@ internal class TokenDetailsLoadedBalanceConverter(
return when (status) {
is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencySymbol)
is CryptoCurrencyStatus.NoQuote -> MarketPriceBlockState.Error(currencySymbol)
is CryptoCurrencyStatus.NoAccount -> {
if (status.fiatRate == null) {
MarketPriceBlockState.Error(currencySymbol)
} else {
status.toContentConfig(currencySymbol)
}
}
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.NoAccount,
is CryptoCurrencyStatus.Unreachable,
is CryptoCurrencyStatus.NoAmount,
-> MarketPriceBlockState.Content(
currencySymbol = currencySymbol,
price = formatPrice(status, appCurrencyProvider()),
priceChangeConfig = PriceChangeState.Content(
valueInPercent = formatPriceChange(status),
type = getPriceChangeType(status),
),
)
-> status.toContentConfig(currencySymbol)
}
}
private fun CryptoCurrencyStatus.Status.toContentConfig(currencySymbol: String): MarketPriceBlockState.Content {
return MarketPriceBlockState.Content(
currencySymbol = currencySymbol,
price = formatPrice(status = this, appCurrency = appCurrencyProvider()),
priceChangeConfig = PriceChangeState.Content(
valueInPercent = formatPriceChange(status = this),
type = getPriceChangeType(status = this),
),
)
}
private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType {
val priceChange = status.priceChange ?: return PriceChangeType.DOWN

View file

@ -6,6 +6,7 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.*
import androidx.paging.cachedIn
import arrow.core.getOrElse
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.Provider
import com.tangem.core.analytics.api.AnalyticsEventHandler
@ -397,7 +398,7 @@ internal class TokenDetailsViewModel @Inject constructor(
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
network = cryptoCurrency.network,
currency = cryptoCurrency,
addressType = AddressType.Default,
),
)
@ -429,7 +430,7 @@ internal class TokenDetailsViewModel @Inject constructor(
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
network = cryptoCurrency.network,
currency = cryptoCurrency,
addressType = AddressType.valueOf(addressModel.type.name),
),
)
@ -438,12 +439,18 @@ internal class TokenDetailsViewModel @Inject constructor(
}
override fun onTransactionClick(txHash: String) {
router.openUrl(
url = getExplorerTransactionUrlUseCase(
txHash = txHash,
networkId = cryptoCurrency.network.id,
),
)
val blockchain = Blockchain.fromId(cryptoCurrency.network.id.value)
// TODO: Fix ton tx urls [REDACTED_TASK_KEY]
if (blockchain == Blockchain.TON || blockchain == Blockchain.TONTestnet) {
return
} else {
router.openUrl(
url = getExplorerTransactionUrlUseCase(
txHash = txHash,
networkId = cryptoCurrency.network.id,
),
)
}
}
override fun onRefreshSwipe() {

View file

@ -323,7 +323,7 @@ internal object WalletPreviewData {
persistentListOf(
WalletManageButton.Buy(enabled = true, onClick = {}),
WalletManageButton.Send(enabled = true, onClick = {}),
WalletManageButton.Receive(onClick = {}),
WalletManageButton.Receive(enabled = true, onClick = {}),
WalletManageButton.Sell(enabled = true, onClick = {}),
WalletManageButton.Swap(enabled = true, onClick = {}),
)

View file

@ -15,6 +15,9 @@ import com.tangem.feature.wallet.impl.R
@Immutable
internal sealed class WalletManageButton(val config: ActionButtonConfig) {
/** Is click enabled */
abstract val enabled: Boolean
/** Lambda be invoked when manage button is clicked */
abstract val onClick: () -> Unit
@ -24,7 +27,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
* @property enabled button click availability
* @property onClick lambda be invoked when Buy button is clicked
*/
data class Buy(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton(
data class Buy(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_buy),
iconResId = R.drawable.ic_plus_24,
@ -39,7 +42,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
* @property enabled button click availability
* @property onClick lambda be invoked when Send button is clicked
*/
data class Send(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton(
data class Send(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_send),
iconResId = R.drawable.ic_arrow_up_24,
@ -53,12 +56,12 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
*
* @property onClick lambda be invoked when Receive button is clicked
*/
data class Receive(override val onClick: () -> Unit) : WalletManageButton(
data class Receive(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_receive),
iconResId = R.drawable.ic_arrow_down_24,
onClick = onClick,
enabled = true,
enabled = enabled,
),
)
@ -68,7 +71,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
* @property enabled button click availability
* @property onClick lambda be invoked when Sell button is clicked
*/
data class Sell(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton(
data class Sell(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_sell),
iconResId = R.drawable.ic_currency_24,
@ -83,7 +86,7 @@ internal sealed class WalletManageButton(val config: ActionButtonConfig) {
* @property enabled button click availability
* @property onClick lambda be invoked when Swap button is clicked
*/
data class Swap(val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton(
data class Swap(override val enabled: Boolean, override val onClick: () -> Unit) : WalletManageButton(
config = ActionButtonConfig(
text = TextReference.Res(id = R.string.common_swap),
iconResId = R.drawable.ic_exchange_vertical_24,

View file

@ -42,6 +42,7 @@ internal class WalletCryptoCurrencyActionsConverter(
}
is TokenActionsState.ActionState.Receive -> {
WalletManageButton.Receive(
enabled = action.enabled,
onClick = { clickIntents.onReceiveClick(cryptoCurrencyStatus) },
)
}

View file

@ -72,7 +72,7 @@ internal class WalletLockedConverter(
is WalletManageButton.Sell -> button.copy(enabled = false)
is WalletManageButton.Send -> button.copy(enabled = false)
is WalletManageButton.Swap -> button.copy(enabled = false)
is WalletManageButton.Receive -> button
is WalletManageButton.Receive -> button.copy(enabled = false)
}
}
.toPersistentList()

View file

@ -43,7 +43,7 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
state.copy(
walletsListConfig = getUpdatedSelectedWallet(status = status.value, state = state),
marketPriceBlockState = getMarketPriceState(status = status.value, currencyName = currencyName),
marketPriceBlockState = getMarketPriceState(status = status.value, currencySymbol = currencyName),
)
}
is WalletMultiCurrencyState.Content,
@ -54,32 +54,27 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
}
}
private fun getMarketPriceState(status: CryptoCurrencyStatus.Status, currencyName: String): MarketPriceBlockState {
private fun getMarketPriceState(
status: CryptoCurrencyStatus.Status,
currencySymbol: String,
): MarketPriceBlockState {
return when (status) {
is CryptoCurrencyStatus.NoQuote -> MarketPriceBlockState.Error(currencyName)
is CryptoCurrencyStatus.Loaded,
is CryptoCurrencyStatus.NoAmount,
-> MarketPriceBlockState.Content(
currencySymbol = currencyName,
price = formatPrice(status, appCurrencyProvider()),
priceChangeConfig = PriceChangeState.Content(
valueInPercent = formatPriceChange(status),
type = getPriceChangeType(status),
),
)
is CryptoCurrencyStatus.NoAccount -> MarketPriceBlockState.Content(
currencySymbol = currencyName,
price = formatPrice(status, appCurrencyProvider()),
priceChangeConfig = PriceChangeState.Content(
valueInPercent = formatPriceChange(status),
type = getPriceChangeType(status),
),
)
is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencyName)
-> status.toContentConfig(currencySymbol)
is CryptoCurrencyStatus.NoAccount -> {
if (status.fiatRate == null) {
MarketPriceBlockState.Error(currencySymbol)
} else {
status.toContentConfig(currencySymbol)
}
}
is CryptoCurrencyStatus.Loading -> MarketPriceBlockState.Loading(currencySymbol)
is CryptoCurrencyStatus.Custom,
is CryptoCurrencyStatus.MissedDerivation,
is CryptoCurrencyStatus.Unreachable,
-> MarketPriceBlockState.Error(currencyName)
is CryptoCurrencyStatus.NoQuote,
-> MarketPriceBlockState.Error(currencySymbol)
}
}
@ -138,6 +133,17 @@ internal class WalletSingleCurrencyLoadedBalanceConverter(
)
}
private fun CryptoCurrencyStatus.Status.toContentConfig(currencySymbol: String): MarketPriceBlockState.Content {
return MarketPriceBlockState.Content(
currencySymbol = currencySymbol,
price = formatPrice(status = this, appCurrency = appCurrencyProvider()),
priceChangeConfig = PriceChangeState.Content(
valueInPercent = formatPriceChange(status = this),
type = getPriceChangeType(status = this),
),
)
}
private fun getPriceChangeType(status: CryptoCurrencyStatus.Status): PriceChangeType {
val priceChange = status.priceChange ?: return PriceChangeType.DOWN

View file

@ -148,7 +148,7 @@ internal class WalletSkeletonStateConverter(
return persistentListOf(
WalletManageButton.Buy(enabled = false, onClick = {}),
WalletManageButton.Send(enabled = false, onClick = {}),
WalletManageButton.Receive(onClick = {}),
WalletManageButton.Receive(enabled = false, onClick = {}),
WalletManageButton.Sell(enabled = false, onClick = {}),
)
}

View file

@ -1,6 +1,8 @@
package com.tangem.feature.wallet.presentation.wallet.ui
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.widget.Toast
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.material3.SnackbarHostState
@ -57,7 +59,7 @@ internal fun WalletEventEffect(
.addOnCompleteListener {
handleOnCompleteRequestTask(
reviewManager = reviewManager,
activity = context as? Activity ?: return@addOnCompleteListener,
activity = context.findActivity(),
task = it,
onDismissClick = value.onDismissClick,
)
@ -69,6 +71,15 @@ internal fun WalletEventEffect(
)
}
private fun Context.findActivity(): Activity {
var context = this
while (context is ContextWrapper) {
if (context is Activity) return context
context = context.baseContext
}
error("Permissions should be called in the context of an Activity")
}
private fun handleOnCompleteRequestTask(
reviewManager: ReviewManager,
activity: Activity,

View file

@ -646,10 +646,23 @@ internal class WalletViewModel @Inject constructor(
event = TokenScreenAnalyticsEvent.ButtonSend(cryptoCurrencyStatus.currency.symbol),
)
val currency = cryptoCurrencyStatus.currency as? CryptoCurrency.Token ?: return
viewModelScope.launch(dispatchers.io) {
val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
val userWallet = getWallet(index = state.walletsListConfig.selectedWalletIndex)
when (cryptoCurrencyStatus.currency) {
is CryptoCurrency.Coin -> {
uiState = stateFactory.getStateWithClosedBottomSheet()
reduxStateHolder.dispatch(
action = TradeCryptoAction.New.SendCoin(
userWallet = userWallet,
coinStatus = cryptoCurrencyStatus,
),
)
}
is CryptoCurrency.Token -> sendToken(userWallet, cryptoCurrencyStatus)
}
}
private fun sendToken(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus) {
viewModelScope.launch(dispatchers.io) {
getNetworkCoinStatusUseCase(
userWalletId = userWallet.walletId,
networkId = cryptoCurrencyStatus.currency.network.id,
@ -659,10 +672,11 @@ internal class WalletViewModel @Inject constructor(
.take(count = 1)
.collectLatest {
it.onRight { coinStatus ->
uiState = stateFactory.getStateWithClosedBottomSheet()
reduxStateHolder.dispatch(
action = TradeCryptoAction.New.SendToken(
userWallet = userWallet,
tokenCurrency = currency,
tokenCurrency = requireNotNull(cryptoCurrencyStatus.currency as? CryptoCurrency.Token),
tokenFiatRate = cryptoCurrencyStatus.value.fiatRate,
coinFiatRate = coinStatus.value.fiatRate,
),
@ -805,18 +819,18 @@ internal class WalletViewModel @Inject constructor(
private fun openExplorer() {
val state = uiState as? WalletState.ContentState ?: return
val currencyNetwork = singleWalletCryptoCurrencyStatus?.currency?.network ?: return
val currency = singleWalletCryptoCurrencyStatus?.currency ?: return
viewModelScope.launch(dispatchers.main) {
val userWalletId = getWallet(state.walletsListConfig.selectedWalletIndex).walletId
val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currencyNetwork)
val addresses = walletManagersFacade.getAddress(userWalletId = userWalletId, network = currency.network)
if (addresses.size == 1) {
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
network = currencyNetwork,
currency = currency,
addressType = AddressType.Default,
),
)
@ -834,7 +848,7 @@ internal class WalletViewModel @Inject constructor(
onClick = {
onAddressTypeSelected(
userWalletId = userWalletId,
currencyNetwork = currencyNetwork,
currency = currency,
addressModel = it,
)
},
@ -846,14 +860,14 @@ internal class WalletViewModel @Inject constructor(
private fun onAddressTypeSelected(
userWalletId: UserWalletId,
currencyNetwork: Network,
currency: CryptoCurrency,
addressModel: AddressModel,
) {
viewModelScope.launch(dispatchers.main) {
router.openUrl(
url = getExploreUrlUseCase(
userWalletId = userWalletId,
network = currencyNetwork,
currency = currency,
addressType = AddressType.valueOf(addressModel.type.name),
),
)

View file

@ -81,7 +81,7 @@ spr-client = "3.6.2"
# endregion Other libraries
# region Tangem
tangemBlockchainSdk = "release-app_5.0-365"
tangemBlockchainSdk = "release-app_5.0-368"
#tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds
tangemCardSdk = "release-app_5.0-308"
#tangemCardSdk = "0.0.1" # Keep it! - used for local builds