Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-21 12:08:16 +03:00
parent ce878fdbe2
commit 18fe9257cb
14 changed files with 23 additions and 908 deletions

View file

@ -20,10 +20,10 @@ import com.tangem.core.decompose.navigation.inner.InnerNavigation
import com.tangem.core.decompose.navigation.inner.InnerNavigationState
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.child.create.OnboardingNoteCreateWalletComponent
import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteCommonState
import com.tangem.features.onboarding.v2.note.impl.route.ONBOARDING_NOTE_STEPS_COUNT
@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.StateFlow
internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
@Assisted context: AppComponentContext,
@Assisted val params: OnboardingNoteComponent.Params,
val onboardingDoneComponentFactory: OnboardingDoneComponent.Factory,
) : OnboardingNoteComponent, AppComponentContext by context {
private val model: OnboardingNoteModel = getOrCreateModel(params)
@ -98,14 +99,14 @@ internal class DefaultOnboardingNoteComponent @AssistedInject constructor(
childParams = childParams,
onWalletCreated = { userWallet ->
model.onWalletCreated(userWallet)
model.stackNavigation.push(OnboardingNoteRoute.TopUp)
model.stackNavigation.push(OnboardingNoteRoute.Done)
},
),
)
OnboardingNoteRoute.TopUp -> OnboardingNoteTopUpComponent(
appComponentContext = factoryContext,
params = OnboardingNoteTopUpComponent.Params(
childParams = childParams,
OnboardingNoteRoute.Done -> onboardingDoneComponentFactory.create(
context = factoryContext,
params = OnboardingDoneComponent.Params(
mode = OnboardingDoneComponent.Mode.WalletCreated,
onDone = { params.onDone() },
),
)

View file

@ -1,39 +0,0 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup
import androidx.activity.compose.BackHandler
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel
import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.OnboardingNoteTopUp
internal class OnboardingNoteTopUpComponent(
appComponentContext: AppComponentContext,
private val params: Params,
) : ComposableContentComponent, AppComponentContext by appComponentContext {
private val model: OnboardingNoteTopUpModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
BackHandler(onBack = remember(this) { { params.childParams.onBack() } })
OnboardingNoteTopUp(
modifier = modifier,
state = state,
)
}
data class Params(
val childParams: DefaultOnboardingNoteComponent.ChildParams,
val onDone: () -> Unit,
)
}

View file

@ -1,253 +0,0 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.model
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.core.analytics.Analytics
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.exchange.RampStateManager
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.network.NetworkAddress
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.ScenarioUnavailabilityReason
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.onboarding.v2.common.analytics.OnboardingEvent
import com.tangem.features.onboarding.v2.note.impl.child.topup.OnboardingNoteTopUpComponent
import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.isPositive
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@Suppress("LongParameterList")
@ModelScoped
internal class OnboardingNoteTopUpModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase,
private val urlOpener: UrlOpener,
private val clipboardManager: ClipboardManager,
private val shareManager: ShareManager,
private val rampStateManager: RampStateManager,
private val cardRepository: CardRepository,
private val saveWalletUseCase: SaveWalletUseCase,
private val walletBalanceFetcher: WalletBalanceFetcher,
private val tokensFeatureToggles: TokensFeatureToggles,
) : Model() {
private val params = paramsContainer.require<OnboardingNoteTopUpComponent.Params>()
private val commonState = params.childParams.commonState
private val scanResponse = params.childParams.commonState.value.scanResponse
private var userWallet = params.childParams.commonState.value.userWallet
private val _uiState = MutableStateFlow(
OnboardingNoteTopUpUM(
onRefreshBalanceClick = ::refreshBalance,
onBuyCryptoClick = ::onBuyCryptoClick,
onShowWalletAddressClick = ::onShowWalletAddressClick,
onDismissBottomSheet = ::onDismissBottomSheet,
),
)
val uiState: StateFlow<OnboardingNoteTopUpUM> = _uiState
init {
Analytics.send(OnboardingEvent.Topup.ScreenOpened)
observeArtwork()
modelScope.launch {
createUserWalletIfNull()
cardRepository.finishCardActivation(scanResponse.card.cardId)
observeCryptoCurrencyStatus()
refreshBalance()
}
}
private fun refreshBalance() {
modelScope.launch {
showBalanceLoadingProgress(true)
createUserWalletIfNull()
val userWalletId = requireNotNull(userWallet?.walletId)
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWalletId))
.onLeft(Timber::e)
} else {
fetchCurrencyStatusUseCase(userWalletId = userWalletId, refresh = true)
}
showBalanceLoadingProgress(false)
}
}
private fun onBuyCryptoClick() {
val cryptoCurrencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return
modelScope.launch {
getLegacyTopUpUrlUseCase(cryptoCurrencyStatus).onRight {
urlOpener.openUrl(it)
}
}
Analytics.send(OnboardingEvent.Topup.ButtonBuyCrypto(cryptoCurrencyStatus.currency))
}
private fun onShowWalletAddressClick() {
val currencyStatus = params.childParams.commonState.value.cryptoCurrencyStatus ?: return
val networkAddress = currencyStatus.value.networkAddress ?: return
_uiState.update {
it.copy(addressBottomSheetConfig = createReceiveBS(currencyStatus, networkAddress))
}
Analytics.send(OnboardingEvent.Topup.ButtonShowWalletAddress)
}
private fun onDismissBottomSheet() {
_uiState.update {
it.copy(addressBottomSheetConfig = null)
}
}
private suspend fun createUserWalletIfNull() {
if (userWallet != null) {
return
}
val commonState = params.childParams.commonState.value
userWallet = commonState.userWallet ?: createAndSaveUserWallet(scanResponse)
}
private fun observeArtwork() {
modelScope.launch {
params.childParams.commonState.collect {
_uiState.value = _uiState.value.copy(
cardArtwork = it.cardArtwork,
)
}
}
}
private fun observeCryptoCurrencyStatus() {
val userWalletId = userWallet?.walletId ?: return
getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWalletId = userWalletId)
.map { it.getOrNull() }
.filterNotNull()
.onEach(::applyCryptoCurrencyStatusToState)
.launchIn(modelScope)
}
private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) {
if (commonState.value.cryptoCurrencyStatus == null) {
loadAvailableForBuy(status)
}
commonState.update {
it.copy(cryptoCurrencyStatus = status)
}
val amount = when (status.value) {
is CryptoCurrencyStatus.Loaded -> status.value.amount
is CryptoCurrencyStatus.NoAccount -> status.value.amount
is CryptoCurrencyStatus.NoQuote -> status.value.amount
else -> null
}
val hasCurrentNetworkTransactions = when (status.value) {
is CryptoCurrencyStatus.Loaded -> status.value.hasCurrentNetworkTransactions
is CryptoCurrencyStatus.NoAccount -> status.value.hasCurrentNetworkTransactions
else -> false
}
val amountToCreateAccount = (status.value as? CryptoCurrencyStatus.NoAccount)?.amountToCreateAccount
if (amount?.isPositive() == true || hasCurrentNetworkTransactions) {
params.onDone()
}
_uiState.update {
it.copy(
amountToCreateAccount = amountToCreateAccount
?.format {
crypto(
symbol = status.currency.symbol,
decimals = status.currency.decimals,
)
},
balance = amount?.format {
crypto(
symbol = status.currency.symbol,
decimals = status.currency.decimals,
)
}.orEmpty(),
isTopUpDataLoading = status.value.networkAddress == null,
)
}
}
private fun showBalanceLoadingProgress(value: Boolean) {
_uiState.update {
it.copy(isRefreshing = value)
}
}
private fun loadAvailableForBuy(cryptoCurrencyStatus: CryptoCurrencyStatus) {
modelScope.launch {
val availableForBuy = rampStateManager.availableForBuy(
userWallet = userWallet ?: return@launch,
cryptoCurrency = cryptoCurrencyStatus.currency,
)
_uiState.update {
it.copy(
availableForBuy = availableForBuy == ScenarioUnavailabilityReason.None,
availableForBuyLoading = false,
)
}
}
}
private fun createReceiveBS(currencyStatus: CryptoCurrencyStatus, networkAddress: NetworkAddress) =
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = uiState.value.onDismissBottomSheet,
content = TokenReceiveBottomSheetConfig(
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
name = currencyStatus.currency.name,
symbol = currencyStatus.currency.symbol,
),
network = currencyStatus.currency.network,
networkAddress = networkAddress,
showMemoDisclaimer =
currencyStatus.currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
onCopyClick = {
Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currencyStatus.currency.symbol))
clipboardManager.setText(text = it, isSensitive = true)
},
onShareClick = {
Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currencyStatus.currency.symbol))
shareManager.shareText(text = it)
},
),
)
private suspend fun createAndSaveUserWallet(scanResponse: ScanResponse): UserWallet {
val wallet = requireNotNull(
value = coldUserWalletBuilderFactory.create(scanResponse = scanResponse).build(),
lazyMessage = { "User wallet not created" },
)
saveWalletUseCase(wallet, false)
return wallet
}
}

View file

@ -1,102 +0,0 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.LocalTangemShimmer
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.common.ui.RefreshButton
import com.tangem.features.onboarding.v2.common.ui.WalletCard
import com.tangem.features.onboarding.v2.impl.R
import com.valentinilk.shimmer.shimmer
@Composable
fun OnboardingNoteTopUpHeader(
balance: String,
cardArtwork: ArtworkUM?,
isRefreshing: Boolean,
onRefreshBalanceClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.heightIn(min = 180.dp)
.widthIn(max = 450.dp),
contentAlignment = Alignment.Center,
) {
Box(
modifier = Modifier
.padding(vertical = 24.dp, horizontal = 16.dp)
.fillMaxSize()
.background(
TangemTheme.colors.button.secondary,
shape = TangemTheme.shapes.roundedCornersMedium,
),
contentAlignment = Alignment.Center,
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(horizontal = 32.dp),
) {
SpacerHMax()
Text(
text = stringResourceSafe(R.string.common_balance_title),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
modifier = if (balance.isEmpty()) {
Modifier
.width(120.dp)
.clip(RoundedCornerShape(size = TangemTheme.dimens.radius3))
.shimmer(LocalTangemShimmer.current)
} else {
Modifier
},
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
text = balance,
)
SpacerHMax()
}
}
WalletCard(
modifier = Modifier.width(120.dp).align(Alignment.TopCenter),
artwork = cardArtwork,
)
RefreshButton(
modifier = Modifier.align(Alignment.BottomCenter),
isRefreshing = isRefreshing,
onRefreshBalanceClick = onRefreshBalanceClick,
)
}
}
@Preview(showBackground = true)
@Composable
private fun OnboardinNoteTopUpHeaderPreview() {
TangemThemePreview {
OnboardingNoteTopUpHeader(
balance = "0.00000001 BTC",
cardArtwork = ArtworkUM(null, ""),
onRefreshBalanceClick = {},
isRefreshing = false,
)
}
}

View file

@ -1,130 +0,0 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
import com.tangem.core.ui.components.SpacerH16
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.note.impl.ALL_STEPS_TOP_CONTAINER_WEIGHT
import com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state.OnboardingNoteTopUpUM
@Composable
fun OnboardingNoteTopUp(state: OnboardingNoteTopUpUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
verticalArrangement = Arrangement.Bottom,
horizontalAlignment = Alignment.CenterHorizontally,
) {
OnboardingNoteTopUpHeader(
balance = state.balance,
cardArtwork = state.cardArtwork,
onRefreshBalanceClick = state.onRefreshBalanceClick,
isRefreshing = state.isRefreshing,
modifier = Modifier
.padding(top = 64.dp)
.padding(horizontal = 24.dp)
.weight(ALL_STEPS_TOP_CONTAINER_WEIGHT)
.fillMaxWidth(),
)
Column(
modifier = Modifier.weight(1 - ALL_STEPS_TOP_CONTAINER_WEIGHT)
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = 32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerHMax()
Text(
text = stringResourceSafe(R.string.onboarding_topup_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
)
val text = if (state.amountToCreateAccount != null) {
stringResourceSafe(
R.string.onboarding_top_up_min_create_account_amount,
state.amountToCreateAccount,
)
} else {
stringResourceSafe(R.string.onboarding_top_up_body)
}
SpacerH16()
Text(
text = text,
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerHMax()
}
BottomButtons(state)
state.addressBottomSheetConfig?.let { config ->
TokenReceiveBottomSheet(config = config)
}
}
}
@Composable
private fun BottomButtons(state: OnboardingNoteTopUpUM) {
if (state.availableForBuy) {
PrimaryButton(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 8.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto),
onClick = state.onBuyCryptoClick,
)
} else {
PrimaryButton(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.onboarding_button_receive_crypto),
onClick = state.onShowWalletAddressClick,
)
}
AnimatedVisibility(visible = !state.availableForBuyLoading) {
if (state.availableForBuy) {
SecondaryButton(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address),
onClick = state.onShowWalletAddressClick,
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun OnboardingNoteTopUpPreview() {
TangemThemePreview {
OnboardingNoteTopUp(
state = OnboardingNoteTopUpUM(
availableForBuy = true,
),
)
}
}

View file

@ -1,19 +0,0 @@
package com.tangem.features.onboarding.v2.note.impl.child.topup.ui.state
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
data class OnboardingNoteTopUpUM(
val cardArtwork: ArtworkUM? = null,
val availableForBuy: Boolean = false,
val availableForBuyLoading: Boolean = true,
val balance: String = "",
val isRefreshing: Boolean = false,
val isTopUpDataLoading: Boolean = true,
val amountToCreateAccount: String? = null,
val addressBottomSheetConfig: TangemBottomSheetConfig? = null,
val onBuyCryptoClick: () -> Unit = {},
val onShowWalletAddressClick: () -> Unit = {},
val onRefreshBalanceClick: () -> Unit = {},
val onDismissBottomSheet: () -> Unit = {},
)

View file

@ -5,7 +5,6 @@ import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.note.api.OnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.DefaultOnboardingNoteComponent
import com.tangem.features.onboarding.v2.note.impl.child.create.model.OnboardingNoteCreateWalletModel
import com.tangem.features.onboarding.v2.note.impl.child.topup.model.OnboardingNoteTopUpModel
import com.tangem.features.onboarding.v2.note.impl.model.OnboardingNoteModel
import dagger.Binds
import dagger.Module
@ -37,9 +36,4 @@ internal interface ModelModule {
@IntoMap
@ClassKey(OnboardingNoteCreateWalletModel::class)
fun provideNoteCreateWalletModel(model: OnboardingNoteCreateWalletModel): Model
@Binds
@IntoMap
@ClassKey(OnboardingNoteTopUpModel::class)
fun provideNoteTopUpModel(model: OnboardingNoteTopUpModel): Model
}

View file

@ -84,7 +84,7 @@ internal class OnboardingNoteModel @Inject constructor(
return if (card.wallets.isEmpty()) {
OnboardingNoteRoute.CreateWallet
} else {
OnboardingNoteRoute.TopUp
OnboardingNoteRoute.Done
}
}

View file

@ -9,7 +9,7 @@ internal sealed class OnboardingNoteRoute {
data object CreateWallet : OnboardingNoteRoute()
@Serializable
data object TopUp : OnboardingNoteRoute()
data object Done : OnboardingNoteRoute()
}
internal const val ONBOARDING_NOTE_STEPS_COUNT = 3
internal const val ONBOARDING_NOTE_STEPS_COUNT = 2

View file

@ -2,5 +2,5 @@ package com.tangem.features.onboarding.v2.note.impl.route
internal fun OnboardingNoteRoute.stepNum() = when (this) {
OnboardingNoteRoute.CreateWallet -> 1
OnboardingNoteRoute.TopUp -> 2
OnboardingNoteRoute.Done -> 2
}

View file

@ -7,40 +7,22 @@ import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.hexToBytes
import com.tangem.common.extensions.toHexString
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheetConfig
import com.tangem.core.analytics.Analytics
import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.navigation.share.ShareManager
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.core.ui.clipboard.ClipboardManager
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.toWrappedList
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.datasource.local.config.issuers.IssuersConfigStorage
import com.tangem.domain.card.common.util.twinsIsTwinned
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.common.TwinCardNumber
import com.tangem.domain.common.getTwinCardNumber
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.currency.CryptoCurrencyStatus
import com.tangem.domain.models.network.Network
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.onboarding.SaveTwinsOnboardingShownUseCase
import com.tangem.domain.onramp.GetLegacyTopUpUrlUseCase
import com.tangem.domain.tokens.FetchCurrencyStatusUseCase
import com.tangem.domain.tokens.GetSingleCryptoCurrencyStatusUseCase
import com.tangem.domain.tokens.TokensFeatureToggles
import com.tangem.domain.tokens.model.analytics.TokenReceiveAnalyticsEvent
import com.tangem.domain.tokens.wallet.WalletBalanceFetcher
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.builder.UserWalletIdBuilder
import com.tangem.domain.wallets.usecase.DeleteWalletUseCase
@ -61,11 +43,9 @@ import com.tangem.utils.coroutines.saveIn
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import java.math.BigDecimal
import javax.inject.Inject
@Suppress("LongParameterList", "LargeClass")
@ -81,16 +61,8 @@ internal class OnboardingTwinModel @Inject constructor(
private val tangemSdkManager: TangemSdkManager,
private val issuersConfigStorage: IssuersConfigStorage,
private val cardRepository: CardRepository,
private val getSingleCryptoCurrencyStatusUseCase: GetSingleCryptoCurrencyStatusUseCase,
private val fetchCurrencyStatusUseCase: FetchCurrencyStatusUseCase,
private val getLegacyTopUpUrlUseCase: GetLegacyTopUpUrlUseCase,
private val urlOpener: UrlOpener,
private val uiMessageSender: UiMessageSender,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val clipboardManager: ClipboardManager,
private val shareManager: ShareManager,
private val tokensFeatureToggles: TokensFeatureToggles,
private val walletBalanceFetcher: WalletBalanceFetcher,
) : Model() {
private val params = paramsContainer.require<OnboardingTwinComponent.Params>()
@ -116,14 +88,10 @@ internal class OnboardingTwinModel @Inject constructor(
)
}
Mode.CreateWallet -> {
if (params.scanResponse.twinsIsTwinned()) {
OnboardingTwinUM.TopUpPrepare
} else {
OnboardingTwinUM.Welcome(
pairCardNumber = firstCardTwinNumber.pairNumber().number,
onContinueClick = ::navigateToFirstScan,
)
}
OnboardingTwinUM.Welcome(
pairCardNumber = firstCardTwinNumber.pairNumber().number,
onContinueClick = ::navigateToFirstScan,
)
}
},
)
@ -139,11 +107,6 @@ internal class OnboardingTwinModel @Inject constructor(
saveTwinsOnboardingShownUseCase()
}
}
OnboardingTwinUM.TopUpPrepare -> {
modelScope.launch {
setTopUpState(params.scanResponse)
}
}
else -> {}
}
}
@ -218,10 +181,7 @@ internal class OnboardingTwinModel @Inject constructor(
},
)
}
innerNavigationState.update {
it.copy(stackSize = 2)
}
innerNavigationState.update { it.copy(stackSize = 2) }
}
}
}
@ -229,7 +189,6 @@ internal class OnboardingTwinModel @Inject constructor(
private fun createSecondWallet(firstPublicKey: String) {
setLoading(true)
modelScope.launch {
val secondCardNumber = firstCardTwinNumber.pairNumber().number
val result = tangemSdkManager.createSecondTwinWallet(
@ -318,13 +277,13 @@ internal class OnboardingTwinModel @Inject constructor(
Mode.CreateWallet -> {
modelScope.launch {
setLoading(true)
setTopUpState(scanResponse)
finishActivation(scanResponse)
}.saveIn(cryptoCurrencyStatusJobHolder)
}
}
}
private suspend fun setTopUpState(scanResponse: ScanResponse) = coroutineScope {
private suspend fun finishActivation(scanResponse: ScanResponse) = coroutineScope {
val userWallet = coldUserWalletBuilderFactory.create(scanResponse).build() ?: run {
Timber.e("User wallet not created")
setLoading(false)
@ -342,117 +301,7 @@ internal class OnboardingTwinModel @Inject constructor(
cardRepository.finishCardActivation(params.scanResponse.card.cardId)
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))
} else {
fetchCurrencyStatusUseCase.invoke(userWalletId = userWallet.walletId, refresh = true)
}
.onLeft {
Timber.e("Unable to fetch currency status: $it")
setLoading(false)
}
val cryptoCurrencyStatus = getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId)
.firstOrNull()?.getOrNull()
?: run {
setLoading(false)
Timber.e("Unable to get currency status")
return@coroutineScope
}
launch {
getSingleCryptoCurrencyStatusUseCase.invokeSingleWallet(userWallet.walletId)
.collect {
it.onRight { status ->
applyCryptoCurrencyStatusToState(status)
}
}
}
_uiState.value = OnboardingTwinUM.TopUp(
onBuyCryptoClick = { onBuyCryptoClick(cryptoCurrencyStatus) },
onRefreshClick = { onRefreshBalanceClick(userWallet) },
onShowAddressClick = { onShowAddressClick(cryptoCurrencyStatus) },
isLoading = true,
)
innerNavigationState.update {
it.copy(stackSize = 4)
}
}
private fun applyCryptoCurrencyStatusToState(status: CryptoCurrencyStatus) {
val amount = (status.value as? CryptoCurrencyStatus.Loaded)?.amount ?: return
if (amount > BigDecimal.ZERO) {
params.modelCallbacks.onDone()
} else {
update<OnboardingTwinUM.TopUp> {
it.copy(
balance = BigDecimal.ZERO.format { crypto(status.currency) },
onBuyCryptoClick = { onBuyCryptoClick(status) },
onShowAddressClick = { onShowAddressClick(status) },
isLoading = false,
)
}
}
}
private fun onBuyCryptoClick(status: CryptoCurrencyStatus) {
modelScope.launch {
getLegacyTopUpUrlUseCase(status).onRight {
urlOpener.openUrl(it)
}
}
}
private fun onShowAddressClick(status: CryptoCurrencyStatus) {
val currency = status.currency
val networkAddress = status.value.networkAddress ?: return
update<OnboardingTwinUM.TopUp> {
it.copy(
bottomSheetConfig = TangemBottomSheetConfig(
isShown = true,
onDismissRequest = {
update<OnboardingTwinUM.TopUp> {
it.copy(bottomSheetConfig = TangemBottomSheetConfig.Empty)
}
},
content = TokenReceiveBottomSheetConfig(
asset = TokenReceiveBottomSheetConfig.Asset.Currency(
name = currency.name,
symbol = currency.symbol,
),
network = currency.network,
networkAddress = networkAddress,
showMemoDisclaimer =
currency.network.transactionExtrasType != Network.TransactionExtrasType.NONE,
onCopyClick = {
Analytics.send(TokenReceiveAnalyticsEvent.ButtonCopyAddress(currency.symbol))
clipboardManager.setText(text = it, isSensitive = true)
},
onShareClick = {
Analytics.send(TokenReceiveAnalyticsEvent.ButtonShareAddress(currency.symbol))
shareManager.shareText(text = it)
},
),
),
)
}
}
private fun onRefreshBalanceClick(userWallet: UserWallet) {
update<OnboardingTwinUM.TopUp> {
it.copy(isLoading = true)
}
modelScope.launch {
if (tokensFeatureToggles.isWalletBalanceFetcherEnabled) {
walletBalanceFetcher(params = WalletBalanceFetcher.Params(userWalletId = userWallet.walletId))
.onLeft(Timber::e)
} else {
fetchCurrencyStatusUseCase(userWalletId = userWallet.walletId, refresh = true)
}
}
params.modelCallbacks.onDone()
}
private fun saveWalletAndDone() {

View file

@ -18,9 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH16
import com.tangem.common.ui.bottomsheet.receive.TokenReceiveBottomSheet
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.core.ui.res.TangemTheme
@ -43,11 +41,6 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi
.weight(.48f)
.fillMaxWidth(),
state = state.artwork,
balance = (state as? OnboardingTwinUM.TopUp)?.balance ?: "",
isRefreshing = state.isLoading,
onRefreshBalanceClick = {
(state as? OnboardingTwinUM.TopUp)?.onRefreshClick()
},
)
AnimatedContent(
@ -60,16 +53,10 @@ internal fun OnboardingTwin(state: OnboardingTwinUM, modifier: Modifier = Modifi
when (st) {
is OnboardingTwinUM.ResetWarning -> ResetWarning(st)
is OnboardingTwinUM.ScanCard -> ScanCard(st)
is OnboardingTwinUM.TopUp -> TopUp(st)
is OnboardingTwinUM.Welcome -> Welcome(st)
OnboardingTwinUM.TopUpPrepare -> {}
}
}
}
if (state is OnboardingTwinUM.TopUp) {
TokenReceiveBottomSheet(config = state.bottomSheetConfig)
}
}
@Suppress("LongMethod")
@ -154,55 +141,6 @@ private fun ResetWarning(state: OnboardingTwinUM.ResetWarning, modifier: Modifie
}
}
@Composable
private fun TopUp(state: OnboardingTwinUM.TopUp, modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Column(
modifier = Modifier
.padding(start = 32.dp, end = 32.dp, bottom = 16.dp)
.weight(1f)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Text(
text = stringResourceSafe(R.string.onboarding_topup_title),
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
style = TangemTheme.typography.h2,
)
SpacerH16()
Text(
text = stringResourceSafe(R.string.onboarding_top_up_body),
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
style = TangemTheme.typography.body1,
)
}
PrimaryButton(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 12.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.onboarding_top_up_button_but_crypto),
onClick = state.onBuyCryptoClick,
)
SecondaryButton(
modifier = Modifier
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.fillMaxWidth(),
text = stringResourceSafe(R.string.onboarding_top_up_button_show_wallet_address),
onClick = state.onShowAddressClick,
)
}
}
@Composable
private fun ScanCard(state: OnboardingTwinUM.ScanCard, modifier: Modifier = Modifier) {
Column(
@ -287,14 +225,6 @@ private fun Welcome(state: OnboardingTwinUM.Welcome, modifier: Modifier = Modifi
}
}
@Preview(showBackground = true)
@Composable
private fun PreviewTopUp() {
TangemThemePreview {
OnboardingTwin(OnboardingTwinUM.TopUp())
}
}
@Preview(showBackground = true)
@Composable
private fun PreviewWelcome() {

View file

@ -1,12 +1,10 @@
package com.tangem.features.onboarding.v2.twin.impl.ui
import android.annotation.SuppressLint
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Transition
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
@ -16,21 +14,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import com.tangem.core.ui.components.SpacerH8
import com.tangem.core.ui.components.SpacerHMax
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.wallets.models.Artwork
import com.tangem.features.onboarding.v2.common.ui.RefreshButton
import com.tangem.features.onboarding.v2.common.ui.WalletCard
import com.tangem.features.onboarding.v2.impl.R
import kotlinx.coroutines.delay
import java.util.concurrent.TimeUnit
@ -45,8 +37,6 @@ internal sealed class TwinWalletArtworkUM {
FirstCard, SecondCard
}
}
data object TopUp : TwinWalletArtworkUM()
}
private data class CardsTransitionState(
@ -64,15 +54,10 @@ private data class WalletCardTransitionState(
val zIndex: Float = 0f,
)
@SuppressLint("UnusedBoxWithConstraintsScope")
@Suppress("LongMethod")
@Composable
internal fun TwinWalletArtworks(
state: TwinWalletArtworkUM,
balance: String,
isRefreshing: Boolean,
onRefreshBalanceClick: () -> Unit,
modifier: Modifier = Modifier,
) {
internal fun TwinWalletArtworks(state: TwinWalletArtworkUM, modifier: Modifier = Modifier) {
BoxWithConstraints(
modifier
.heightIn(min = 180.dp)
@ -110,22 +95,6 @@ internal fun TwinWalletArtworks(
}
}
AnimatedVisibility(
visible = state == TwinWalletArtworkUM.TopUp,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(
modifier = Modifier
.padding(vertical = 24.dp, horizontal = 16.dp)
.fillMaxSize()
.background(
TangemTheme.colors.button.secondary,
shape = TangemTheme.shapes.roundedCornersMedium,
),
)
}
AnimatedTwinCards(
transition1 = transition1,
transition2 = transition2,
@ -133,46 +102,6 @@ internal fun TwinWalletArtworks(
.widthIn(max = 450.dp)
.matchParentSize(),
)
AnimatedVisibility(
modifier = Modifier.align(Alignment.Center),
visible = state == TwinWalletArtworkUM.TopUp,
enter = fadeIn(),
exit = fadeOut(),
) {
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
SpacerHMax()
Text(
text = stringResourceSafe(R.string.common_balance_title),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
SpacerH8()
Text(
text = balance,
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
SpacerHMax()
}
}
AnimatedVisibility(
modifier = Modifier.align(Alignment.BottomCenter),
visible = state == TwinWalletArtworkUM.TopUp,
enter = fadeIn(),
exit = fadeOut(),
) {
RefreshButton(
isRefreshing = isRefreshing,
onRefreshBalanceClick = onRefreshBalanceClick,
)
}
}
}
@ -308,26 +237,6 @@ private fun TwinWalletArtworkUM.toTransitionSetState(
)
}
}
TwinWalletArtworkUM.TopUp -> {
val scale = 0.4f
val yTranslation = -maxHeightDp * density - 24 * density
listOf(
CardsTransitionState(
walletCard1 = WalletCardTransitionState(
yTranslation = yTranslation,
xScale = scale,
yScale = scale,
zIndex = 2f,
),
walletCard2 = WalletCardTransitionState(
yTranslation = yTranslation * 0.35f,
xScale = scale * 0.8f,
yScale = scale * 0.8f,
zIndex = 1f,
),
),
)
}
}
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@ -341,16 +250,13 @@ private fun Preview() {
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.TopUp) }
var state: TwinWalletArtworkUM by remember { mutableStateOf(TwinWalletArtworkUM.Spread) }
TwinWalletArtworks(
state = state,
modifier = Modifier
.padding(top = 250.dp)
.fillMaxWidth(),
balance = "1 USD",
isRefreshing = false,
onRefreshBalanceClick = {},
)
var index by remember { mutableIntStateOf(0) }
@ -366,7 +272,6 @@ private fun Preview() {
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard),
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.FirstCard),
TwinWalletArtworkUM.Leapfrog(step = TwinWalletArtworkUM.Leapfrog.Step.SecondCard),
TwinWalletArtworkUM.TopUp,
)
state = list[index % list.size]

View file

@ -1,7 +1,6 @@
package com.tangem.features.onboarding.v2.twin.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.features.onboarding.v2.twin.impl.ui.TwinWalletArtworkUM
@Immutable
@ -11,12 +10,6 @@ internal sealed class OnboardingTwinUM {
abstract val isLoading: Boolean
abstract val artwork: TwinWalletArtworkUM
data object TopUpPrepare : OnboardingTwinUM() {
override val stepIndex: Int = 0
override val isLoading: Boolean = false
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Spread
}
data class Welcome(
override val isLoading: Boolean = false,
val pairCardNumber: Int = 2,
@ -56,23 +49,9 @@ internal sealed class OnboardingTwinUM {
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.Leapfrog(artworkStep)
}
data class TopUp(
override val isLoading: Boolean = false,
val balance: String = "",
val bottomSheetConfig: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
val onBuyCryptoClick: () -> Unit = {},
val onShowAddressClick: () -> Unit = {},
val onRefreshClick: () -> Unit = {},
) : OnboardingTwinUM() {
override val stepIndex: Int = 2
override val artwork: TwinWalletArtworkUM = TwinWalletArtworkUM.TopUp
}
fun copySealed(isLoading: Boolean = this.isLoading): OnboardingTwinUM = when (this) {
is Welcome -> copy(isLoading = isLoading)
is ResetWarning -> copy()
is ScanCard -> copy(isLoading = isLoading)
is TopUp -> copy(isLoading = isLoading)
TopUpPrepare -> this
}
}