Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-22 17:20:18 +03:00
commit 7db00540c5
351 changed files with 6478 additions and 1691 deletions

View file

@ -60,7 +60,7 @@ internal class AccountCreateEditModel @Inject constructor(
private val umBuilder = AccountCreateEditUMBuilder(params)
val uiState: StateFlow<AccountCreateEditUM>
field = MutableStateFlow(value = getInitialState())
field = MutableStateFlow(value = getInitialState())
init {
if (params is AccountCreateEditComponent.Params.Create) {

View file

@ -36,7 +36,7 @@ internal class AccountSelectorModel @Inject constructor(
private val selectorController get() = params.controller
internal val state: StateFlow<AccountSelectorUM>
field = MutableStateFlow<AccountSelectorUM>(emptyState())
field = MutableStateFlow<AccountSelectorUM>(emptyState())
init {
balanceFetcher.data

View file

@ -61,14 +61,14 @@ internal class CreateWalletSelectionModel @Inject constructor(
) : Model() {
internal val uiState: StateFlow<CreateWalletSelectionUM>
field = MutableStateFlow(
CreateWalletSelectionUM(
onBackClick = { router.pop() },
onMobileWalletClick = ::onMobileWalletClick,
onHardwareWalletClick = ::onHardwareWalletClick,
onScanClick = ::onScanClick,
),
)
field = MutableStateFlow(
CreateWalletSelectionUM(
onBackClick = { router.pop() },
onMobileWalletClick = ::onMobileWalletClick,
onHardwareWalletClick = ::onHardwareWalletClick,
onScanClick = ::onScanClick,
),
)
init {
showAlreadyHaveWalletWithDelay()

View file

@ -140,11 +140,11 @@ internal fun CreateWalletSelectionContent(state: CreateWalletSelectionUM, modifi
@Composable
private fun WalletBlock(
modifier: Modifier = Modifier,
title: String,
description: String,
badge: @Composable () -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
badge: @Composable () -> Unit,
) {
Column(
modifier = modifier

View file

@ -95,11 +95,11 @@ fun StoriesTextAnimation(
@Composable
fun StoriesBottomImageAnimation(
firstStepDuration: Int,
totalDuration: Int,
initialScale: Float = 2.5f,
secondStageScale: Float = SCALE_SWITCH_BARRIER,
targetScale: Float = 1.0f,
firstStepDuration: Int,
totalDuration: Int,
content: @Composable (Modifier) -> Unit,
) {
val secondStepDuration = totalDuration - firstStepDuration

View file

@ -25,10 +25,7 @@ internal class AccessCodeComponent @AssistedInject constructor(
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
if (!state.isConfirmMode) {
DisableScreenshotsDisposableEffect()
}
DisableScreenshotsDisposableEffect()
AccessCode(
modifier = modifier,
state = state,

View file

@ -56,7 +56,7 @@ internal class AccessCodeModel @Inject constructor(
private val params = paramsContainer.require<AccessCodeComponent.Params>()
internal val uiState: StateFlow<AccessCodeUM>
field = MutableStateFlow(getInitialState())
field = MutableStateFlow(getInitialState())
private fun getInitialState() = AccessCodeUM(
accessCode = "",

View file

@ -43,7 +43,7 @@ internal class HotAccessCodeRequestModel @Inject constructor(
)
val uiState: StateFlow<HotAccessCodeRequestUM>
field = MutableStateFlow(getInitialState())
field = MutableStateFlow(getInitialState())
suspend fun show(attemptRequest: HotWalletPasswordRequester.AttemptRequest) {
if (userWalletExists(attemptRequest.hotWalletId).not()) {

View file

@ -7,6 +7,7 @@ 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.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent
@ -22,6 +23,7 @@ internal class AddExistingWalletImportComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
DisableScreenshotsDisposableEffect()
AddExistingWalletImportContent(
state = state,
modifier = modifier,

View file

@ -80,7 +80,7 @@ internal class AddExistingWalletImportModel @Inject constructor(
}
internal val uiState: StateFlow<AddExistingWalletImportUM>
field = MutableStateFlow(importSeedPhraseUiStateBuilder.getState())
field = MutableStateFlow(importSeedPhraseUiStateBuilder.getState())
@Suppress("UnusedPrivateMember")
private fun importWallet(mnemonic: Mnemonic, passphrase: String?) {

View file

@ -67,13 +67,17 @@ internal class ImportSeedPhraseUiStateBuilder(
val text = st.words.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val newWords = wordsFromText.dropLast(1) + word
val newWordsText = newWords.joinToString(" ")
st.copy(
words = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
),
val newWordsText = newWords.joinToString(" ").plus(" ")
val newWordsState = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
)
st.copy(
words = newWordsState,
).also {
launchInterceptWords(wordsField = newWordsState)
suggestNextWord(newWordsState)
}
}
}

View file

@ -63,16 +63,16 @@ internal class AddExistingWalletStartModel @Inject constructor(
private val params: AddExistingWalletStartComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<AddExistingWalletStartUM>
field = MutableStateFlow(
AddExistingWalletStartUM(
showWantToPurchaseBlock = false,
isScanInProgress = false,
onBackClick = params.callbacks::onBackClick,
onImportPhraseClick = params.callbacks::onImportPhraseClick,
onScanCardClick = ::onScanClick,
onBuyCardClick = ::onShopClick,
),
)
field = MutableStateFlow(
AddExistingWalletStartUM(
showWantToPurchaseBlock = false,
isScanInProgress = false,
onBackClick = params.callbacks::onBackClick,
onImportPhraseClick = params.callbacks::onImportPhraseClick,
onScanCardClick = ::onScanClick,
onBuyCardClick = ::onShopClick,
),
)
init {
showWantToPurchaseBlockWithDelay()

View file

@ -28,13 +28,13 @@ internal class CreateMobileWalletModel @Inject constructor(
) : Model() {
internal val uiState: StateFlow<CreateMobileWalletUM>
field = MutableStateFlow(
CreateMobileWalletUM(
onBackClick = { router.pop() },
onCreateClick = ::onCreateClick,
createButtonLoading = false,
),
)
field = MutableStateFlow(
CreateMobileWalletUM(
onBackClick = { router.pop() },
onCreateClick = ::onCreateClick,
createButtonLoading = false,
),
)
private fun onCreateClick() {
modelScope.launch {

View file

@ -7,6 +7,7 @@ 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.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel
import com.tangem.features.hotwallet.manualbackup.check.ui.ManualBackupCheckContent
@ -22,6 +23,7 @@ internal class ManualBackupCheckComponent @AssistedInject constructor(
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
DisableScreenshotsDisposableEffect()
ManualBackupCheckContent(
state = state,
modifier = modifier,

View file

@ -44,7 +44,7 @@ internal class ManualBackupCheckModel @Inject constructor(
private val callbacks = params.callbacks
internal val uiState: StateFlow<ManualBackupCheckUM>
field = MutableStateFlow(getInitialUIState())
field = MutableStateFlow(getInitialUIState())
init {
modelScope.launch {

View file

@ -18,9 +18,9 @@ internal class ManualBackupCompletedModel @Inject constructor(
private val params: ManualBackupCompletedComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<ManualBackupCompletedUM>
field = MutableStateFlow(
ManualBackupCompletedUM(
onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) },
),
)
field = MutableStateFlow(
ManualBackupCompletedUM(
onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) },
),
)
}

View file

@ -35,11 +35,11 @@ internal class ManualBackupPhraseModel @Inject constructor(
private val callbacks = params.callbacks
internal val uiState: StateFlow<ManualBackupPhraseUM>
field = MutableStateFlow(
ManualBackupPhraseUM(
onContinueClick = callbacks::onContinueClick,
),
)
field = MutableStateFlow(
ManualBackupPhraseUM(
onContinueClick = callbacks::onContinueClick,
),
)
init {
modelScope.launch {

View file

@ -18,9 +18,9 @@ internal class ManualBackupStartModel @Inject constructor(
private val params: ManualBackupStartComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<ManualBackupStartUM>
field = MutableStateFlow(
ManualBackupStartUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
field = MutableStateFlow(
ManualBackupStartUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
}

View file

@ -18,9 +18,9 @@ internal class MobileWalletSetupFinishedModel @Inject constructor(
private val params: MobileWalletSetupFinishedComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<MobileWalletSetupFinishedUM>
field = MutableStateFlow(
MobileWalletSetupFinishedUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
field = MutableStateFlow(
MobileWalletSetupFinishedUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
}

View file

@ -20,7 +20,7 @@ internal class HotWalletStepperModel @Inject constructor(
val params = paramsContainer.require<HotWalletStepperComponent.Params>()
val uiState: StateFlow<HotWalletStepperComponent.StepperUM>
field = MutableStateFlow(params.initState)
field = MutableStateFlow(params.initState)
fun updateState(newState: HotWalletStepperComponent.StepperUM) {
uiState.value = newState

View file

@ -35,11 +35,11 @@ internal class ViewPhraseModel @Inject constructor(
private val params = paramsContainer.require<ViewPhraseComponent.Params>()
internal val uiState: StateFlow<ViewPhraseUM>
field = MutableStateFlow(
ViewPhraseUM(
onBackClick = { router.pop() },
),
)
field = MutableStateFlow(
ViewPhraseUM(
onBackClick = { router.pop() },
),
)
init {
loadSeedPhrase()

View file

@ -63,7 +63,7 @@ internal class WalletActivationModel @Inject constructor(
is WalletActivationRoute.ManualBackupCheck -> stackNavigation.pop()
is WalletActivationRoute.ManualBackupCompleted -> Unit
is WalletActivationRoute.SetAccessCode -> Unit
is WalletActivationRoute.ConfirmAccessCode -> Unit
is WalletActivationRoute.ConfirmAccessCode -> stackNavigation.pop()
is WalletActivationRoute.PushNotifications -> Unit
is WalletActivationRoute.SetupFinished -> Unit
}

View file

@ -33,24 +33,24 @@ internal class WalletBackupModel @Inject constructor(
private val params: WalletBackupComponent.Params = paramsContainer.require()
val uiState: StateFlow<WalletBackupUM>
field = MutableStateFlow(
WalletBackupUM(
onBackClick = { router.pop() },
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
field = MutableStateFlow(
WalletBackupUM(
onBackClick = { router.pop() },
recoveryPhraseOption = LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
),
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_coming_soon),
style = LabelStyle.REGULAR,
),
googleDriveStatus = BackupStatus.ComingSoon,
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
onGoogleDriveClick = { },
onHardwareWalletClick = ::onHardwareWalletClick,
backedUp = false,
),
googleDriveOption = LabelUM(
text = resourceReference(R.string.common_coming_soon),
style = LabelStyle.REGULAR,
),
googleDriveStatus = BackupStatus.ComingSoon,
onRecoveryPhraseClick = ::onRecoveryPhraseClick,
onGoogleDriveClick = { },
onHardwareWalletClick = ::onHardwareWalletClick,
backedUp = false,
),
)
)
init {
getWalletUseCase.invoke(params.userWalletId)

View file

@ -81,7 +81,7 @@ internal class ChooseManagedTokensModel @Inject constructor(
val bottomSheetNavigation: SlotNavigation<ChooseManageTokensBottomSheetConfig> = SlotNavigation()
val uiState: StateFlow<ChooseManagedTokenUM>
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
field = MutableStateFlow<ChooseManagedTokenUM>(createReadContentModel())
init {
manageTokensListManager.uiItems

View file

@ -83,6 +83,11 @@ internal class ManageTokensListManager @AssistedInject constructor(
.distinctUntilChanged()
val uiItems: Flow<ImmutableList<CurrencyItemUM>> = uiManager.items
/**
* Launch pagination flow to get currencies
*
* @param isCollapsed set initial display state of networks. !!! WARNING !!! Use `false` flag with cation
*/
suspend fun launchPagination(isCollapsed: Boolean) = coroutineScope {
val loadUserTokensFromRemote = when (mode) {
is ManageTokensMode.Wallet -> source == ManageTokensSource.ONBOARDING

View file

@ -58,8 +58,8 @@ internal fun MarketsTokenDetailsContent(
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
backButtonEnabled: Boolean,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
) {
Content(
modifier = modifier,
@ -88,8 +88,8 @@ private fun Content(
onBackClick: () -> Unit,
onHeaderSizeChange: (Dp) -> Unit,
backButtonEnabled: Boolean,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
modifier: Modifier = Modifier,
portfolioBlock: @Composable ((Modifier) -> Unit)?,
) {
val density = LocalDensity.current
val bottomBarHeight = with(density) { WindowInsets.systemBars.getBottom(this).toDp() }

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.icons.IconTint
import com.tangem.core.ui.components.token.TokenItem
import com.tangem.core.ui.components.token.state.TokenItemState
import com.tangem.core.ui.haptic.TangemHapticEffect
@ -100,9 +101,9 @@ private class PortfolioTokenUMProvider : CollectionPreviewParameterProvider<Port
tokenItemState = (tokenUM.tokenItemState as TokenItemState.Content).copy(
fiatAmountState = contentFiatAmount.copy(
icons = persistentListOf(
TokenItemState.FiatAmountState.Content.IconUM(
TokenFiatAmountState.Content.IconUM(
iconRes = R.drawable.ic_staking_24,
useAccentColor = true,
tint = IconTint.Accent,
),
),
),

View file

@ -185,9 +185,9 @@ internal fun NFTDetailsGroupBlock(
@Composable
internal fun NFTBlocksGroupAction(
text: TextReference,
startIcon: @Composable RowScope.() -> Unit,
onClick: () -> Unit,
modifier: Modifier = Modifier,
startIcon: @Composable RowScope.() -> Unit,
) {
val interactionSource = remember { MutableInteractionSource() }

View file

@ -17,7 +17,7 @@ interface NFTSendSuccessListener {
internal class DefaultNFTSendSuccessTrigger @Inject constructor() : NFTSendSuccessTrigger, NFTSendSuccessListener {
override val nftSendSuccessFlow: SharedFlow<Unit>
field = MutableSharedFlow<Unit>()
field = MutableSharedFlow<Unit>()
override suspend fun triggerSuccessNFTSend() {
nftSendSuccessFlow.emit(Unit)

View file

@ -219,7 +219,7 @@ internal class OnboardingEntryModel @Inject constructor(
// legacy flow
if (userWalletsListManager.hasUserWallets) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLocked!! }.getOrElse { false }
if (isLocked) {
router.replaceAll(AppRoute.Welcome())

View file

@ -14,8 +14,8 @@ import com.tangem.features.onboarding.v2.entry.impl.routing.OnboardingRoute
@Composable
internal inline fun OnboardingEntry(
modifier: Modifier = Modifier,
childStack: ChildStack<OnboardingRoute, Any>,
modifier: Modifier = Modifier,
stepperContent: @Composable (Modifier) -> Unit,
) {
Column(

View file

@ -7,6 +7,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@ -19,6 +20,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.StoriesScreenTestTags
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.createwallet.ui.state.MultiWalletCreateWalletUM
@ -60,7 +62,9 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier:
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 16.dp),
modifier = Modifier
.padding(top = 16.dp)
.testTag(StoriesScreenTestTags.TITLE),
)
Text(
@ -68,7 +72,9 @@ internal fun MultiWalletCreateWallet(state: MultiWalletCreateWalletUM, modifier:
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.padding(top = 12.dp),
modifier = Modifier
.padding(top = 12.dp)
.testTag(StoriesScreenTestTags.TEXT),
)
}

View file

@ -58,13 +58,17 @@ internal class ImportSeedPhraseUiStateBuilder(
val text = st.words.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val newWords = wordsFromText.dropLast(1) + word
val newWordsText = newWords.joinToString(" ")
st.copy(
words = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
),
val newWordsText = newWords.joinToString(" ").plus(" ")
val newWordsState = TextFieldValue(
text = newWordsText,
selection = TextRange(newWordsText.length),
)
st.copy(
words = newWordsState,
).also {
launchInterceptWords(wordsField = newWordsState)
suggestNextWord(newWordsState)
}
}
}

View file

@ -9,5 +9,4 @@ internal enum class OnrampOperation {
BUY,
SELL,
SWAP,
;
}

View file

@ -18,16 +18,16 @@ internal class DefaultFeeSelectorReloadTrigger @Inject constructor() :
FeeSelectorCheckReloadListener {
override val reloadTriggerFlow: SharedFlow<FeeSelectorData>
field = MutableSharedFlow()
field = MutableSharedFlow()
override val loadingStateTriggerFlow: SharedFlow<Unit>
field = MutableSharedFlow()
field = MutableSharedFlow()
override val checkReloadTriggerFlow: SharedFlow<Unit>
field = MutableSharedFlow()
field = MutableSharedFlow()
override val checkReloadResultFlow: SharedFlow<Boolean>
field = MutableSharedFlow()
field = MutableSharedFlow()
override suspend fun triggerUpdate(feeData: FeeSelectorData) {
reloadTriggerFlow.emit(feeData)

View file

@ -62,7 +62,7 @@ internal class FeeSelectorModel @Inject constructor(
val feeSelectorBottomSheet = SlotNavigation<Unit>()
val uiState: StateFlow<FeeSelectorUM>
field = MutableStateFlow<FeeSelectorUM>(params.state)
field = MutableStateFlow<FeeSelectorUM>(params.state)
init {
initAppCurrency()

View file

@ -127,7 +127,7 @@ internal class SendConfirmModel @Inject constructor(
val uiState = _uiState.asStateFlow()
val isBalanceHiddenFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
field = MutableStateFlow(false)
private val amountState
get() = uiState.value.amountUM as? AmountState.Data

View file

@ -106,10 +106,10 @@ internal class SendModel @Inject constructor(
val analyticCategoryName = CommonSendAnalyticEvents.SEND_CATEGORY
val uiState: StateFlow<SendUM>
field = MutableStateFlow(initialState())
field = MutableStateFlow(initialState())
val isBalanceHiddenFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
field = MutableStateFlow(false)
val initialRoute = if (params.amount == null) {
if (uiState.value.isRedesignEnabled) {

View file

@ -88,10 +88,10 @@ internal class NFTSendModel @Inject constructor(
private val nftAsset = params.nftAsset
val uiState: StateFlow<NFTSendUM>
field = MutableStateFlow(initialState())
field = MutableStateFlow(initialState())
val isBalanceHiddenFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
field = MutableStateFlow(false)
var cryptoCurrency: CryptoCurrency by Delegates.notNull()
var userWallet: UserWallet by Delegates.notNull()

View file

@ -79,7 +79,7 @@ internal class SendAmountModel @Inject constructor(
private var isAvailableForSwap: Boolean = false
val isSendWithSwapAvailable: StateFlow<Boolean>
field = MutableStateFlow(false)
field = MutableStateFlow(false)
private val analyticsCategoryName = params.analyticsCategoryName
private var cryptoCurrencyStatus: CryptoCurrencyStatus = CryptoCurrencyStatus(

View file

@ -45,19 +45,19 @@ internal class DefaultSwapAmountUpdateTrigger @Inject constructor() :
SwapAmountReduceListener {
override val updateAmountTriggerFlow: SharedFlow<Pair<String, Boolean>>
field = MutableSharedFlow<Pair<String, Boolean>>()
field = MutableSharedFlow<Pair<String, Boolean>>()
override val reduceToTriggerFlow: SharedFlow<BigDecimal>
field = MutableSharedFlow<BigDecimal>()
field = MutableSharedFlow<BigDecimal>()
override val reduceByTriggerFlow: SharedFlow<ReduceByData>
field = MutableSharedFlow<ReduceByData>()
field = MutableSharedFlow<ReduceByData>()
override val ignoreReduceTriggerFlow: SharedFlow<Unit>
field = MutableSharedFlow<Unit>()
field = MutableSharedFlow<Unit>()
override val reloadQuotesTriggerFlow: Flow<Unit>
field = MutableSharedFlow<Unit>()
field = MutableSharedFlow<Unit>()
override suspend fun triggerUpdateAmount(amountValue: String, isEnterInFiatSelected: Boolean) {
updateAmountTriggerFlow.emit(amountValue to isEnterInFiatSelected)

View file

@ -114,7 +114,7 @@ internal class SwapAmountModel @Inject constructor(
private var showBestRateAnimation: Boolean = false
val uiState: StateFlow<SwapAmountUM>
field = MutableStateFlow(params.amountUM)
field = MutableStateFlow(params.amountUM)
private val amountDebouncer = Debouncer()
private val quoteTaskScheduler = SingleTaskScheduler<Unit>()

View file

@ -37,7 +37,7 @@ internal class SwapChooseProviderModel @Inject constructor(
}
val uiState: StateFlow<SwapChooseProviderBottomSheetContent>
field: MutableStateFlow<SwapChooseProviderBottomSheetContent> = MutableStateFlow(getInitialState())
field: MutableStateFlow<SwapChooseProviderBottomSheetContent> = MutableStateFlow(getInitialState())
fun onProviderClick(quoteUM: SwapQuoteUM) {
params.callback.onProviderResult(quoteUM)

View file

@ -16,7 +16,7 @@ internal class DefaultSwapChooseTokenNetworkTrigger @Inject constructor() :
SwapChooseTokenNetworkListener {
override val swapChooseTokenNetworkResultFlow: SharedFlow<SwapChooseTokenTriggerData>
field = MutableSharedFlow<SwapChooseTokenTriggerData>()
field = MutableSharedFlow<SwapChooseTokenTriggerData>()
override suspend fun trigger(
swapCurrencies: SwapCurrencies,

View file

@ -46,20 +46,20 @@ internal class SwapChooseTokenNetworkModel @Inject constructor(
private val params: SwapChooseTokenNetworkComponent.Params = paramsContainer.require()
val uiState: StateFlow<SwapChooseTokenNetworkUM>
field: MutableStateFlow<SwapChooseTokenNetworkUM> = MutableStateFlow(
SwapChooseTokenNetworkUM(
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = params.onDismiss,
content = SwapChooseTokenNetworkContentUM.Loading(
messageContent = getErrorMessage(
tokenName = params.token.name,
onDismiss = params.onDismiss,
field: MutableStateFlow<SwapChooseTokenNetworkUM> = MutableStateFlow(
SwapChooseTokenNetworkUM(
TangemBottomSheetConfig(
isShown = true,
onDismissRequest = params.onDismiss,
content = SwapChooseTokenNetworkContentUM.Loading(
messageContent = getErrorMessage(
tokenName = params.token.name,
onDismiss = params.onDismiss,
),
),
),
),
),
)
)
init {
initContent()

View file

@ -30,10 +30,10 @@ internal class DefaultSwapNotificationsUpdateTrigger @Inject constructor() :
SwapNotificationsUpdateTrigger {
override val updateTriggerFlow: SharedFlow<SwapNotificationData>
field = MutableSharedFlow<SwapNotificationData>()
field = MutableSharedFlow<SwapNotificationData>()
override val hasErrorFlow: SharedFlow<Boolean>
field = MutableSharedFlow<Boolean>()
field = MutableSharedFlow<Boolean>()
override suspend fun callbackHasError(hasError: Boolean) {
hasErrorFlow.emit(hasError)

View file

@ -38,7 +38,7 @@ internal class SwapNotificationsModel @Inject constructor(
private var notificationData = params.swapNotificationData
val uiState: StateFlow<ImmutableList<NotificationUM>>
field = MutableStateFlow<ImmutableList<NotificationUM>>(persistentListOf())
field = MutableStateFlow<ImmutableList<NotificationUM>>(persistentListOf())
init {
subscribeToNotificationUpdateTrigger()

View file

@ -18,7 +18,6 @@ import com.tangem.core.decompose.context.childByContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.decompose.navigation.inner.InnerRouter
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.decompose.getEmptyComposableContentComponent
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.swap.models.R
import com.tangem.domain.swap.models.SwapDirection
@ -152,9 +151,9 @@ internal class DefaultSendWithSwapComponent @AssistedInject constructor(
private fun getDestinationComponent(factoryContext: AppComponentContext): ComposableContentComponent {
val amountContentUM = model.uiState.value.amountUM as? SwapAmountUM.Content
?: return getEmptyComposableContentComponent()
?: return ComposableContentComponent.EMPTY
val secondaryCryptoCurrency = amountContentUM.secondaryCryptoCurrencyStatus?.currency
?: return getEmptyComposableContentComponent()
?: return ComposableContentComponent.EMPTY
return sendDestinationComponentFactory.create(
context = factoryContext,

View file

@ -91,7 +91,7 @@ internal class SendWithSwapConfirmModel @Inject constructor(
private val params: SendWithSwapConfirmComponent.Params = paramsContainer.require()
val uiState: StateFlow<SendWithSwapUM>
field = MutableStateFlow(params.sendWithSwapUM)
field = MutableStateFlow(params.sendWithSwapUM)
val primaryCurrencyStatus: CryptoCurrencyStatus = params.primaryCryptoCurrencyStatusFlow.value
val secondaryCurrencyStatus: CryptoCurrencyStatus? = amountUM?.secondaryCryptoCurrencyStatus

View file

@ -67,26 +67,26 @@ internal class SendWithSwapModel @Inject constructor(
var appCurrency: AppCurrency = AppCurrency.Default
val uiState: StateFlow<SendWithSwapUM>
field = MutableStateFlow(initialState())
field = MutableStateFlow(initialState())
val isBalanceHiddenFlow: StateFlow<Boolean>
field = MutableStateFlow(false)
field = MutableStateFlow(false)
val primaryCryptoCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
field = MutableStateFlow(
CryptoCurrencyStatus(
currency = params.currency,
value = CryptoCurrencyStatus.Loading,
),
)
field = MutableStateFlow(
CryptoCurrencyStatus(
currency = params.currency,
value = CryptoCurrencyStatus.Loading,
),
)
val primaryFeePaidCurrencyStatusFlow: StateFlow<CryptoCurrencyStatus>
field = MutableStateFlow(
CryptoCurrencyStatus(
currency = params.currency,
value = CryptoCurrencyStatus.Loading,
),
)
field = MutableStateFlow(
CryptoCurrencyStatus(
currency = params.currency,
value = CryptoCurrencyStatus.Loading,
),
)
init {
initUserWallet()

View file

@ -203,8 +203,7 @@ internal class DefaultSwapRepository(
override suspend fun getExchangeStatus(
userWallet: UserWallet,
txId: String,
): Either<UnknownError,
ExchangeStatusModel,> {
): Either<UnknownError, ExchangeStatusModel> {
return withContext(coroutineDispatcher.io) {
either {
catch(

View file

@ -25,6 +25,8 @@ dependencies {
implementation(projects.domain.balanceHiding)
implementation(projects.domain.balanceHiding.models)
implementation(projects.domain.models)
implementation(projects.domain.visa)
implementation(projects.domain.visa.models)
/** Compose */
implementation(deps.compose.foundation)

View file

@ -30,7 +30,7 @@ internal class TangemPayDetailsModel @Inject constructor(
) : Model() {
val uiState: StateFlow<TangemPayDetailsUM>
field = MutableStateFlow(getInitialState())
field = MutableStateFlow(getInitialState())
private val refreshStateJobHolder = JobHolder()

View file

@ -5,16 +5,15 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.balancehiding.GetBalanceHidingSettingsUseCase
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
import com.tangem.features.tangempay.components.txHistory.DefaultTangemPayTxHistoryComponent
import com.tangem.features.tangempay.components.txHistory.PreviewTangemPayTxHistoryComponent
import com.tangem.features.tangempay.utils.TangemPayTxHistoryListManager
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ -24,39 +23,106 @@ import javax.inject.Inject
internal class TangemPayTxHistoryModel @Inject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
override val dispatchers: CoroutineDispatcherProvider,
tangemPayTxHistoryRepository: TangemPayTxHistoryRepository,
paramsContainer: ParamsContainer,
) : Model() {
) : Model(), TxHistoryUiActions {
private val params: DefaultTangemPayTxHistoryComponent.Params = paramsContainer.require()
private val listManager = TangemPayTxHistoryListManager(
repository = tangemPayTxHistoryRepository,
dispatchers = dispatchers,
userWalletId = params.userWalletId,
txHistoryUiActions = this,
)
val uiState: StateFlow<TxHistoryUM>
field = MutableStateFlow(getInitialState())
field = MutableStateFlow<TxHistoryUM>(getLoadingState(isBalanceHidden = true))
init {
handleBalanceHiding()
launchPagination()
subscribeToUiItemChanges()
}
@Suppress("MagicNumber")
private fun launchPagination() {
modelScope.launch { listManager.launchPagination() }
}
private fun subscribeToUiItemChanges() {
modelScope.launch {
Timber.d("subscribeToUiItemChanges: ${params.userWalletId}")
delay(2000)
uiState.update { PreviewTangemPayTxHistoryComponent.contentUM }
listManager.uiItems
.onEach { updateState(it) }
.launchIn(modelScope)
listManager.paginationStatus
.onEach { paginationStatus -> handlePaginationStatus(paginationStatus) }
.launchIn(modelScope)
}
private fun updateState(items: ImmutableList<TxHistoryUM.TxHistoryItemUM>) {
uiState.update { state ->
if (state is TxHistoryUM.Content) {
state.copy(items = items)
} else {
TxHistoryUM.Content(
items = items,
isBalanceHidden = state.isBalanceHidden,
loadMore = ::loadMoreItems,
)
}
}
}
private fun handlePaginationStatus(status: PaginationStatus<*>) {
uiState.update { state ->
when (status) {
is PaginationStatus.InitialLoadingError -> getErrorState(state.isBalanceHidden)
PaginationStatus.EndOfPagination,
PaginationStatus.InitialLoading,
PaginationStatus.NextBatchLoading,
PaginationStatus.None,
is PaginationStatus.Paginating<*>,
-> state
}
}
}
private fun loadMoreItems(): Boolean {
modelScope.launch { listManager.loadMore(params.userWalletId) }
return true
}
fun reload() {
// fast exit
if (uiState.value is TxHistoryUM.NotSupported) return
uiState.update { state ->
state as? TxHistoryUM.Content ?: getLoadingState(state.isBalanceHidden)
}
modelScope.launch { listManager.reload() }
}
private fun handleBalanceHiding() {
getBalanceHidingSettingsUseCase()
.onEach { uiState.update { state -> state.copySealed(isBalanceHidden = it.isBalanceHidden) } }
.launchIn(modelScope)
}
private fun onExploreClick() {
override fun openExplorer() {
Timber.d("onExploreClick: open explorer")
}
private fun getInitialState(): TxHistoryUM {
return TxHistoryUM.Loading(isBalanceHidden = true, onExploreClick = ::onExploreClick)
override fun openTxInExplorer(txHash: String) {
Timber.d("openTxInExplorer: $txHash")
}
private fun getErrorState(isBalanceHidden: Boolean): TxHistoryUM.Error {
return TxHistoryUM.Error(
isBalanceHidden = isBalanceHidden,
onReloadClick = ::reload,
onExploreClick = ::openExplorer,
)
}
private fun getLoadingState(isBalanceHidden: Boolean): TxHistoryUM.Loading {
return TxHistoryUM.Loading(isBalanceHidden = isBalanceHidden, onExploreClick = ::openExplorer)
}
}

View file

@ -0,0 +1,40 @@
package com.tangem.features.tangempay.model.transformers
import com.tangem.core.ui.components.transactions.state.TransactionState
import com.tangem.core.ui.extensions.capitalize
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.utils.DateTimeFormatters
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.details.impl.R
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.utils.StringsSigns
import com.tangem.utils.converter.Converter
import org.joda.time.DateTimeZone
import java.util.Currency
internal class TangemPayTxHistoryItemsConverter(
private val txHistoryUiActions: TxHistoryUiActions,
) : Converter<TangemPayTxHistoryItem, TransactionState> {
override fun convert(value: TangemPayTxHistoryItem): TransactionState {
val localDate = value.date?.withZone(DateTimeZone.getDefault())
val currency = Currency.getInstance(value.currency)
val amount = value.amount.format {
fiat(fiatCurrencyCode = currency.currencyCode, fiatCurrencySymbol = currency.symbol)
}
return TransactionState.Content(
txHash = value.id,
amount = "${StringsSigns.MINUS}$amount",
time = localDate?.let { DateTimeFormatters.formatDate(it, DateTimeFormatters.timeFormatter) } ?: "",
status = TransactionState.Content.Status.Confirmed,
direction = TransactionState.Content.Direction.OUTGOING,
iconRes = R.drawable.ic_arrow_up_24,
title = stringReference(value = value.merchantName?.capitalize() ?: "Unknown merchant"),
subtitle = stringReference("How to get merchant type?"),
timestamp = localDate?.millis ?: 0,
onClick = { txHistoryUiActions.openTxInExplorer(value.id) },
)
}
}

View file

@ -0,0 +1,85 @@
package com.tangem.features.tangempay.utils
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListBatchingContext
import com.tangem.domain.tangempay.model.TangemPayTxHistoryListConfig
import com.tangem.domain.tangempay.repository.TangemPayTxHistoryRepository
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.pagination.BatchAction
import com.tangem.pagination.BatchListState
import com.tangem.pagination.PaginationStatus
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
private typealias TangemPayTxHistoryBatchAction = BatchAction<Int, TangemPayTxHistoryListConfig, Nothing>
internal class TangemPayTxHistoryListManager(
private val repository: TangemPayTxHistoryRepository,
private val dispatchers: CoroutineDispatcherProvider,
private val userWalletId: UserWalletId,
private val txHistoryUiActions: TxHistoryUiActions,
) {
private val jobHolder = JobHolder()
private val actionsFlow: MutableSharedFlow<TangemPayTxHistoryBatchAction> = MutableSharedFlow(
replay = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val state: MutableStateFlow<TangemPayTxHistoryState> = MutableStateFlow(TangemPayTxHistoryState())
private val uiManager = TangemPayTxHistoryUiManager(state = state, txHistoryUiActions = txHistoryUiActions)
val uiItems: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = uiManager.items
val paginationStatus: Flow<PaginationStatus<*>> = state.map { it.status }.distinctUntilChanged()
suspend fun launchPagination() = coroutineScope {
val batchFlow = repository.getTxHistoryBatchFlow(
context = TangemPayTxHistoryListBatchingContext(actionsFlow = actionsFlow, coroutineScope = this),
batchSize = 50,
)
batchFlow.state
.onEach { state -> updateState(state) }
.flowOn(dispatchers.default)
.launchIn(scope = this)
.saveIn(jobHolder)
// Initial load
reload()
}
suspend fun reload() {
actionsFlow.emit(
BatchAction.Reload(
requestParams = TangemPayTxHistoryListConfig(userWalletId = userWalletId, refresh = true),
),
)
}
suspend fun loadMore(userWalletId: UserWalletId) {
actionsFlow.emit(
BatchAction.LoadMore(
requestParams = TangemPayTxHistoryListConfig(userWalletId, refresh = false),
),
)
}
private fun updateState(batchListState: BatchListState<Int, List<TangemPayTxHistoryItem>>) {
state.update { state ->
val clearUiBatches =
state.status is PaginationStatus.InitialLoading && batchListState.status is PaginationStatus.Paginating
state.copy(
status = batchListState.status,
uiBatches = uiManager.createOrUpdateUiBatches(
newCurrencyBatches = batchListState.data,
clearUiBatches = clearUiBatches,
),
)
}
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.features.tangempay.utils
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.pagination.Batch
import com.tangem.pagination.PaginationStatus
data class TangemPayTxHistoryState(
val status: PaginationStatus<*> = PaginationStatus.None,
val uiBatches: List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> = listOf(),
)

View file

@ -0,0 +1,118 @@
package com.tangem.features.tangempay.utils
import com.tangem.core.ui.utils.toDateFormatWithTodayYesterday
import com.tangem.domain.visa.model.TangemPayTxHistoryItem
import com.tangem.features.tangempay.model.transformers.TangemPayTxHistoryItemsConverter
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.txhistory.utils.TxHistoryUiActions
import com.tangem.pagination.Batch
import com.tangem.pagination.PaginationStatus
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.*
import java.util.UUID
internal class TangemPayTxHistoryUiManager(
private val state: MutableStateFlow<TangemPayTxHistoryState>,
private val txHistoryUiActions: TxHistoryUiActions,
) {
@OptIn(ExperimentalCoroutinesApi::class)
val items: Flow<ImmutableList<TxHistoryUM.TxHistoryItemUM>> = state
// filter initial states, since we dont emit loading items as UI items
.filter {
it.status !is PaginationStatus.None &&
it.status !is PaginationStatus.InitialLoading &&
it.status !is PaginationStatus.InitialLoadingError
}
.mapLatest { state ->
state.uiBatches.asSequence()
.flatMap { it.data }
.toImmutableList()
}
.distinctUntilChanged()
private val txHistoryItemConverter = TangemPayTxHistoryItemsConverter(txHistoryUiActions = txHistoryUiActions)
fun createOrUpdateUiBatches(
newCurrencyBatches: List<Batch<Int, List<TangemPayTxHistoryItem>>>,
clearUiBatches: Boolean,
): List<Batch<Int, List<TxHistoryUM.TxHistoryItemUM>>> {
val currentUiBatches = state.value.uiBatches
val batches = if (clearUiBatches) mutableListOf() else currentUiBatches.toMutableList()
for ((key, data) in newCurrencyBatches) {
// Find if batch with same key exists
val existingBatchIndex = batches.indexOfFirst { it.key == key }
val shouldUpdateExisting = existingBatchIndex != -1 &&
currentUiBatches[existingBatchIndex].data.transactionItemsSizeNotEqual(data)
// Case 1: Update existing batch if sizes differ
if (shouldUpdateExisting) {
val items = generateUiItems(key, data)
batches[existingBatchIndex] = Batch(key = key, data = items)
continue
}
// Case 2: Skip if batch exists and has same size
if (existingBatchIndex != -1) {
continue
}
// Case 3: Create new batch
val items = generateUiItems(key, data)
batches.add(Batch(key = key, data = items))
}
return batches
}
private fun generateUiItems(key: Int, data: List<TangemPayTxHistoryItem>): List<TxHistoryUM.TxHistoryItemUM> {
val items = mutableListOf<TxHistoryUM.TxHistoryItemUM>()
// Add title for the first batch
if (key == 0) {
items.add(TxHistoryUM.TxHistoryItemUM.Title(onExploreClick = txHistoryUiActions::openExplorer))
}
// Process batch items only if there are any
if (data.isNotEmpty()) {
// Add first item with its group title
val firstItem = data.first()
val firstDate = firstItem.timeStampInMillis.toDateFormatWithTodayYesterday()
items.add(
TxHistoryUM.TxHistoryItemUM.GroupTitle(
title = firstDate,
itemKey = UUID.randomUUID().toString(),
),
)
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(firstItem)))
// Process remaining items with date separators when needed
data.zipWithNext { current, next ->
val currentDate = current.timeStampInMillis.toDateFormatWithTodayYesterday()
val nextDate = next.timeStampInMillis.toDateFormatWithTodayYesterday()
if (currentDate != nextDate) {
items.add(
TxHistoryUM.TxHistoryItemUM.GroupTitle(
title = nextDate,
itemKey = UUID.randomUUID().toString(),
),
)
}
items.add(TxHistoryUM.TxHistoryItemUM.Transaction(txHistoryItemConverter.convert(next)))
}
}
return items
}
private fun List<TxHistoryUM.TxHistoryItemUM>.transactionItemsSizeNotEqual(
txInfos: List<TangemPayTxHistoryItem>,
): Boolean {
return this.filterIsInstance<TxHistoryUM.TxHistoryItemUM.Transaction>().size != txInfos.size
}
}

View file

@ -15,6 +15,8 @@ dependencies {
/** Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.error)
implementation(deps.arrow.core)
/** Common */
implementation(projects.common.routing)
@ -25,6 +27,9 @@ dependencies {
implementation(projects.features.tangempay.details.api)
implementation(projects.features.kyc.api)
/** Domain */
implementation(projects.domain.visa)
/** Compose */
implementation(deps.compose.foundation)
implementation(deps.compose.material3)

View file

@ -5,6 +5,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.context.child
import com.tangem.features.kyc.KycComponent
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.features.tangempay.model.TangemPayOnboardingModel
import dagger.assisted.Assisted
@ -15,14 +17,21 @@ import com.tangem.features.tangempay.ui.TandemPayOnboardingScreen
internal class DefaultTangemPayOnboardingComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: TangemPayOnboardingComponent.Params,
kycComponentFactory: KycComponent.Factory,
) : TangemPayOnboardingComponent, AppComponentContext by appComponentContext {
private val kycComponent = kycComponentFactory.create(child("kycComponent"))
private val model: TangemPayOnboardingModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.screenState.collectAsStateWithLifecycle()
TandemPayOnboardingScreen(modifier = modifier, state = state)
TandemPayOnboardingScreen(modifier = modifier, state = state, onButtonClick = ::onButtonClick)
}
private fun onButtonClick() {
kycComponent.launch()
}
@AssistedFactory

View file

@ -4,23 +4,55 @@ import androidx.compose.runtime.Stable
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.navigation.Router
import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.features.tangempay.components.TangemPayOnboardingComponent
import com.tangem.features.tangempay.ui.TangemPayOnboardingScreenState
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject
@Stable
@ModelScoped
internal class TangemPayOnboardingModel @Inject constructor(
paramsContainer: ParamsContainer,
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
private val repository: OnboardingRepository,
) : Model() {
@Suppress("UnusedPrivateMember")
private val params = paramsContainer.require<TangemPayOnboardingComponent.Params>()
val screenState: StateFlow<TangemPayOnboardingScreenState>
field = MutableStateFlow(TangemPayOnboardingScreenState())
field = MutableStateFlow(TangemPayOnboardingScreenState())
init {
modelScope.launch {
repository.validateDeeplink(params.deeplink)
.onRight { isValid -> if (isValid) checkCustomerInfo() }
.onLeft { exit() }
}
}
private suspend fun checkCustomerInfo() {
repository.getCustomerInfo()
.onRight { customerInfo ->
when {
!customerInfo.isKycApproved() -> {
screenState.value = screenState.value.copy(fullScreenLoading = false)
}
!customerInfo.isProductInstanceActive() -> {
// TODO [REDACTED_TASK_KEY]: create order and poll order status (API is not ready yet)
}
else -> exit()
}
}
.onLeft { exit() }
}
private fun exit() {
router.pop()
}
}

View file

@ -13,7 +13,11 @@ import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.res.TangemThemePreview
@Composable
internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, modifier: Modifier = Modifier) {
internal fun TandemPayOnboardingScreen(
state: TangemPayOnboardingScreenState,
onButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Scaffold(
modifier = modifier,
topBar = {
@ -29,6 +33,7 @@ internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, mo
.padding(paddingValues)
.fillMaxSize(),
state = state,
onButtonClick = onButtonClick,
)
},
)
@ -39,6 +44,6 @@ internal fun TandemPayOnboardingScreen(state: TangemPayOnboardingScreenState, mo
@Composable
private fun PreviewDarkTheme() {
TangemThemePreview {
TandemPayOnboardingScreen(state = TangemPayOnboardingScreenState())
TandemPayOnboardingScreen(state = TangemPayOnboardingScreenState(), {})
}
}

View file

@ -19,7 +19,11 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.tangempay.onboarding.impl.R
@Composable
internal fun TangemPayOnboardingContent(state: TangemPayOnboardingScreenState, modifier: Modifier = Modifier) {
internal fun TangemPayOnboardingContent(
state: TangemPayOnboardingScreenState,
onButtonClick: () -> Unit,
modifier: Modifier = Modifier,
) {
if (state.fullScreenLoading) {
Box(
modifier = modifier.fillMaxSize(),
@ -72,7 +76,7 @@ internal fun TangemPayOnboardingContent(state: TangemPayOnboardingScreenState, m
iconRes = R.drawable.ic_tangem_24,
isIconVisible = true,
showProgress = state.buttonLoading,
onClick = {},
onClick = onButtonClick,
),
)
}

View file

@ -0,0 +1,21 @@
package com.tangem.feature.tester.presentation.common.components.notification
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.notifications.Notification
import com.tangem.core.ui.components.notifications.NotificationConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.feature.tester.impl.R
@Composable
internal fun InitialSetupNotification(subtitle: TextReference, modifier: Modifier = Modifier) {
Notification(
config = NotificationConfig(
subtitle = subtitle,
iconResId = R.drawable.ic_accepted_20,
title = resourceReference(id = R.string.initial_setup_warning_title),
),
modifier = modifier,
)
}

View file

@ -24,6 +24,7 @@ import com.tangem.feature.tester.impl.R
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefresh
import com.tangem.feature.tester.presentation.common.components.appbar.TopBarWithRefreshUM
import com.tangem.feature.tester.presentation.common.components.notification.CustomSetupNotification
import com.tangem.feature.tester.presentation.common.components.notification.InitialSetupNotification
import com.tangem.feature.tester.presentation.featuretoggles.models.TesterFeatureToggle
import com.tangem.feature.tester.presentation.featuretoggles.state.FeatureTogglesContentState
import kotlinx.collections.immutable.persistentListOf
@ -52,8 +53,19 @@ internal fun FeatureTogglesScreen(state: FeatureTogglesContentState) {
),
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp)
.padding(bottom = 8.dp),
.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
} else {
item(key = "warning_notification", contentType = "warning_notification") {
InitialSetupNotification(
subtitle = resourceReference(
id = R.string.feature_toggles_initial_setup_warning_description,
wrappedList(state.appVersion),
),
modifier = Modifier
.animateItem()
.padding(horizontal = 16.dp, vertical = 8.dp),
)
}
}

View file

@ -24,6 +24,5 @@ data class TesterMenuUM(
BLOCKCHAIN_PROVIDERS(R.string.blockchain_providers),
TESTER_ACTIONS(R.string.tester_actions),
TEST_PUSHES(R.string.test_push),
;
}
}

View file

@ -3,7 +3,9 @@
<string name="tester_menu" translatable="false">Tester menu</string>
<string name="feature_toggles" translatable="false">Feature toggles</string>
<string name="custom_setup_warning_title" translatable="false">Custom setup</string>
<string name="initial_setup_warning_title" translatable="false">Initial setup</string>
<string name="feature_toggles_custom_setup_warning_description" translatable="false">Toggles differs from v%s config</string>
<string name="feature_toggles_initial_setup_warning_description" translatable="false">Toggles matches v%s config</string>
<string name="blockchain_providers_custom_setup_warning_description" translatable="false">Providers differs from release config</string>
<string name="restart_app" translatable="false">Restart app</string>
<string name="environment_toggles" translatable="false">Environment toggles</string>

View file

@ -37,21 +37,21 @@ internal class TokenReceiveAssetsModel @Inject constructor(
}
internal val state: StateFlow<ReceiveAssetsUM>
field = MutableStateFlow<ReceiveAssetsUM>(
ReceiveAssetsUM(
onCopyClick = {
params.callback.onCopyClick(
address = it,
source = TokenReceiveCopyActionSource.Receive,
)
},
onOpenQrCodeClick = params.callback::onQrCodeClick,
addresses = params.addresses,
showMemoDisclaimer = params.showMemoDisclaimer,
isEnsResultLoading = false,
notificationConfigs = params.notificationConfigs,
),
)
field = MutableStateFlow<ReceiveAssetsUM>(
ReceiveAssetsUM(
onCopyClick = {
params.callback.onCopyClick(
address = it,
source = TokenReceiveCopyActionSource.Receive,
)
},
onOpenQrCodeClick = params.callback::onQrCodeClick,
addresses = params.addresses,
showMemoDisclaimer = params.showMemoDisclaimer,
isEnsResultLoading = false,
notificationConfigs = params.notificationConfigs,
),
)
private fun configureEnsStatus(): AnalyticsParam.EnsStatus {
val hasEnsAddress = params.addresses.any { it.type == ReceiveAddress.Type.Ens }

View file

@ -56,7 +56,7 @@ internal class TokenReceiveModel @Inject constructor(
val stackNavigation = StackNavigation<TokenReceiveRoutes>()
internal val state: StateFlow<TokenReceiveUM>
field = MutableStateFlow<TokenReceiveUM>(tokenReceiveStateFactory.getInitialState(getTokenName()))
field = MutableStateFlow<TokenReceiveUM>(tokenReceiveStateFactory.getInitialState(getTokenName()))
init {
modelScope.launch {

View file

@ -23,18 +23,18 @@ internal class TokenReceiveQrCodeModel @Inject constructor(
private val params = paramsContainer.require<TokenReceiveQrCodeComponent.TokenReceiveQrCodeParams>()
internal val state: StateFlow<QrCodeUM>
field = MutableStateFlow<QrCodeUM>(
QrCodeUM(
network = params.cryptoCurrency.network.name,
addressValue = params.address.value,
addressName = TextReference.Str("${params.cryptoCurrency.name} (${params.cryptoCurrency.symbol})"),
onCopyClick = {
params.callback.onCopyClick(
address = params.address,
source = TokenReceiveCopyActionSource.QR,
)
},
onShareClick = params.callback::onShareClick,
),
)
field = MutableStateFlow<QrCodeUM>(
QrCodeUM(
network = params.cryptoCurrency.network.name,
addressValue = params.address.value,
addressName = TextReference.Str("${params.cryptoCurrency.name} (${params.cryptoCurrency.symbol})"),
onCopyClick = {
params.callback.onCopyClick(
address = params.address,
source = TokenReceiveCopyActionSource.QR,
)
},
onShareClick = params.callback::onShareClick,
),
)
}

View file

@ -21,11 +21,11 @@ internal class TokenReceiveWarningModel @Inject constructor(
private val params = paramsContainer.require<TokenReceiveWarningComponent.TokenReceiveWarningParams>()
internal val state: StateFlow<WarningUM>
field = MutableStateFlow<WarningUM>(
WarningUM(
iconState = params.iconState,
onWarningAcknowledged = params.callback::onWarningAcknowledged,
network = params.network.name,
),
)
field = MutableStateFlow<WarningUM>(
WarningUM(
iconState = params.iconState,
onWarningAcknowledged = params.callback::onWarningAcknowledged,
network = params.network.name,
),
)
}

View file

@ -102,6 +102,7 @@ dependencies {
implementation(projects.features.txhistory.api)
implementation(projects.features.sendV2.api)
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.yieldSupply.api)
implementation(deps.decompose.ext.compose)

View file

@ -5,8 +5,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.arkivanov.decompose.ComponentContext
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.extensions.compose.subscribeAsState
import com.arkivanov.decompose.router.slot.childSlot
import com.arkivanov.decompose.router.slot.dismiss
import com.arkivanov.essenty.lifecycle.subscribe
import com.tangem.core.decompose.context.AppComponentContext
@ -23,6 +23,7 @@ import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokenreceive.TokenReceiveComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -34,6 +35,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
tokenMarketBlockComponentFactory: TokenMarketBlockComponent.Factory,
txHistoryComponentFactory: TxHistoryComponent.Factory,
private val tokenReceiveComponentFactory: TokenReceiveComponent.Factory,
yieldSupplyComponentFactory: YieldSupplyComponent.Factory,
) : TokenDetailsComponent, AppComponentContext by appComponentContext {
private val model: TokenDetailsModel = getOrCreateModel(params)
@ -67,6 +69,14 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
)
}
private val yieldSupplyComponent = yieldSupplyComponentFactory.create(
context = child("tokenYieldSupplyComponent"),
params = YieldSupplyComponent.Params(
userWalletId = params.userWalletId,
cryptoCurrency = params.currency,
),
)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
@ -76,6 +86,7 @@ internal class DefaultTokenDetailsComponent @AssistedInject constructor(
state = state,
tokenMarketBlockComponent = tokenMarketBlockComponent,
txHistoryComponent = txHistoryComponent,
yieldSupplyComponent = yieldSupplyComponent,
)
bottomSheet.child?.instance?.BottomSheet()
}

View file

@ -19,7 +19,7 @@ internal class DefaultTokenDetailsDeepLinkActionTrigger @Inject constructor() :
TokenDetailsDeepLinkActionListener {
override val tokenDetailsActionFlow: SharedFlow<String>
field = MutableSharedFlow<String>()
field = MutableSharedFlow<String>()
override suspend fun trigger(txId: String) {
tokenDetailsActionFlow.emit(txId)

View file

@ -178,6 +178,7 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = false,
isYieldSupplyFeatureEnabled = false,
)
val tokenDetailsState_2 = TokenDetailsState(
@ -203,6 +204,7 @@ internal object TokenDetailsPreviewData {
bottomSheetConfig = null,
isBalanceHidden = false,
isMarketPriceAvailable = true,
isYieldSupplyFeatureEnabled = true,
)
val tokenDetailsState_3 = tokenDetailsState_2.copy(stakingBlocksState = stakingBalanceBlock)

View file

@ -81,6 +81,7 @@ import com.tangem.features.tokendetails.TokenDetailsComponent
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.tokenreceive.TokenReceiveFeatureToggle
import com.tangem.features.txhistory.entity.TxHistoryContentUpdateEmitter
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.coroutines.*
import com.tangem.utils.extensions.isZero
@ -137,6 +138,7 @@ internal class TokenDetailsModel @Inject constructor(
private val tokenReceiveFeatureToggle: TokenReceiveFeatureToggle,
private val getViewedTokenReceiveWarningUseCase: GetViewedTokenReceiveWarningUseCase,
private val getEnsNameUseCase: GetEnsNameUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
) : Model(), TokenDetailsClickIntents {
private val params = paramsContainer.require<TokenDetailsComponent.Params>()
@ -170,6 +172,7 @@ internal class TokenDetailsModel @Inject constructor(
networkHasDerivationUseCase = networkHasDerivationUseCase,
getUserWalletUseCase = getUserWalletUseCase,
userWalletId = userWalletId,
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
)
private val expressStatusFactory by lazy(mode = LazyThreadSafetyMode.NONE) {

View file

@ -27,6 +27,7 @@ internal sealed class TokenDetailsBalanceBlockState {
val displayFiatBalance: String,
val isBalanceSelectorEnabled: Boolean,
val isBalanceFlickering: Boolean,
val yieldSupplyState: TokenDetailsYieldSupplyState = TokenDetailsYieldSupplyState.Empty,
) : TokenDetailsBalanceBlockState()
data class Error(

View file

@ -23,4 +23,5 @@ internal data class TokenDetailsState(
val bottomSheetConfig: TangemBottomSheetConfig?,
val isBalanceHidden: Boolean,
val isMarketPriceAvailable: Boolean,
val isYieldSupplyFeatureEnabled: Boolean,
)

View file

@ -0,0 +1,8 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.state
sealed class TokenDetailsYieldSupplyState {
data object Empty : TokenDetailsYieldSupplyState()
data class Active(val yieldInfoClick: (() -> Unit)) : TokenDetailsYieldSupplyState()
}

View file

@ -18,6 +18,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.model.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.state.*
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.converter.Converter
import kotlinx.collections.immutable.ImmutableList
@ -29,6 +30,7 @@ internal class TokenDetailsSkeletonStateConverter(
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val userWalletId: UserWalletId,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
) : Converter<CryptoCurrency, TokenDetailsState> {
private val iconStateConverter by lazy { TokenDetailsIconStateConverter() }
@ -69,6 +71,7 @@ internal class TokenDetailsSkeletonStateConverter(
bottomSheetConfig = null,
isBalanceHidden = true,
isMarketPriceAvailable = value.id.rawCurrencyId != null,
isYieldSupplyFeatureEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled,
)
}

View file

@ -31,6 +31,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDeta
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsDialogConfig
import com.tangem.features.tokendetails.impl.R
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.utils.Provider
import kotlinx.collections.immutable.toImmutableList
@ -43,6 +44,7 @@ internal class TokenDetailsStateFactory(
private val networkHasDerivationUseCase: NetworkHasDerivationUseCase,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val userWalletId: UserWalletId,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
) {
private val skeletonStateConverter by lazy {
@ -51,6 +53,7 @@ internal class TokenDetailsStateFactory(
networkHasDerivationUseCase = networkHasDerivationUseCase,
getUserWalletUseCase = getUserWalletUseCase,
userWalletId = userWalletId,
yieldSupplyFeatureToggles = yieldSupplyFeatureToggles,
)
}

View file

@ -40,6 +40,7 @@ import com.tangem.feature.tokendetails.presentation.tokendetails.ui.components.s
import com.tangem.features.markets.token.block.TokenMarketBlockComponent
import com.tangem.features.txhistory.component.TxHistoryComponent
import com.tangem.features.txhistory.entity.TxHistoryUM
import com.tangem.features.yield.supply.api.YieldSupplyComponent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -50,6 +51,7 @@ internal fun TokenDetailsScreen(
state: TokenDetailsState,
tokenMarketBlockComponent: TokenMarketBlockComponent?,
txHistoryComponent: TxHistoryComponent,
yieldSupplyComponent: YieldSupplyComponent,
) {
val bottomBarHeight = with(LocalDensity.current) { WindowInsets.systemBars.getBottom(this).toDp() }
@ -145,6 +147,12 @@ internal fun TokenDetailsScreen(
)
}
if (state.isYieldSupplyFeatureEnabled) {
item {
yieldSupplyComponent.Content(modifier = itemModifier)
}
}
expressTransactionsItems(
expressTxs = state.expressTxsToDisplay,
modifier = itemModifier,
@ -190,6 +198,11 @@ private fun TokenDetailsScreenPreview(
override fun LazyListScope.txHistoryContent(listState: LazyListState, state: TxHistoryUM) = Unit
},
yieldSupplyComponent = object : YieldSupplyComponent {
@Composable
override fun Content(modifier: Modifier) {
}
},
)
}
}

View file

@ -1,8 +1,11 @@
package com.tangem.feature.tokendetails.presentation.tokendetails.ui.components
import android.content.res.Configuration
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Surface
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@ -11,6 +14,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.RectangleShimmer
import com.tangem.core.ui.components.buttons.HorizontalActionChips
import com.tangem.core.ui.components.buttons.segmentedbutton.SegmentedButtons
@ -22,6 +27,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.feature.tokendetails.presentation.tokendetails.TokenDetailsPreviewData
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsBalanceBlockState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.TokenDetailsYieldSupplyState
import com.tangem.feature.tokendetails.presentation.tokendetails.state.components.TokenDetailsActionButton
import com.tangem.features.tokendetails.impl.R
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -44,8 +50,7 @@ internal fun TokenDetailsBalanceBlock(
val spacing12 = TangemTheme.dimens.spacing12
ConstraintLayout(
modifier = Modifier
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
) {
val (balanceTitle, toggleButtons, fiatBalance, cryptoBalance, actionChips) = createRefs()
@ -87,13 +92,12 @@ internal fun TokenDetailsBalanceBlock(
HorizontalActionChips(
buttons = state.actionButtons.map(TokenDetailsActionButton::config).toImmutableList(),
modifier = Modifier
.constrainAs(actionChips) {
top.linkTo(anchor = cryptoBalance.bottom, margin = spacing12)
start.linkTo(anchor = parent.start)
end.linkTo(anchor = parent.end)
bottom.linkTo(anchor = parent.bottom, margin = spacing12)
},
modifier = Modifier.constrainAs(actionChips) {
top.linkTo(anchor = cryptoBalance.bottom, margin = spacing12)
start.linkTo(anchor = parent.start)
end.linkTo(anchor = parent.end)
bottom.linkTo(anchor = parent.bottom, margin = spacing12)
},
containerColor = TangemTheme.colors.background.primary,
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.spacing12),
)
@ -144,14 +148,52 @@ private fun CryptoBalance(
height = TangemTheme.dimens.size16,
),
)
is TokenDetailsBalanceBlockState.Content -> Text(
modifier = modifier,
text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.tertiary,
),
)
is TokenDetailsBalanceBlockState.Content -> {
Crossfade(
modifier = modifier,
targetState = state.yieldSupplyState,
) { yieldSupplyState ->
when (yieldSupplyState) {
is TokenDetailsYieldSupplyState.Active -> {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Icon(
modifier = Modifier.size(TangemTheme.dimens.size16),
painter = painterResource(id = R.drawable.ic_exchange_horizontal_24),
tint = TangemTheme.colors.icon.inactive,
contentDescription = null,
)
Text(
text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.tertiary,
),
)
Icon(
modifier = Modifier
.size(TangemTheme.dimens.size16)
.clickable {
yieldSupplyState.yieldInfoClick()
},
painter = painterResource(id = R.drawable.ic_information_24),
tint = TangemTheme.colors.icon.inactive,
contentDescription = null,
)
}
}
is TokenDetailsYieldSupplyState.Empty -> Text(
text = state.displayCryptoBalance.orMaskWithStars(isBalanceHidden),
style = TangemTheme.typography.caption2.applyBladeBrush(
isEnabled = state.isBalanceFlickering,
textColor = TangemTheme.colors.text.tertiary,
),
)
}
}
}
is TokenDetailsBalanceBlockState.Error -> Text(
modifier = modifier,
text = DASH_SIGN.orMaskWithStars(isBalanceHidden),
@ -209,6 +251,9 @@ private class TokenDetailsBalanceBlockStateProvider : CollectionPreviewParameter
TokenDetailsPreviewData.balanceLoading,
TokenDetailsPreviewData.balanceContent,
TokenDetailsPreviewData.balanceContent.copy(isBalanceFlickering = true),
TokenDetailsPreviewData.balanceContent.copy(
yieldSupplyState = TokenDetailsYieldSupplyState.Active({}),
),
TokenDetailsPreviewData.balanceError,
),
)

View file

@ -86,11 +86,11 @@ private fun CoinIcon(
@Composable
private fun TokenIcon(
url: String?,
alpha: Float,
url: String?,
colorFilter: ColorFilter?,
errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
errorIcon: @Composable () -> Unit,
) {
if (url == null) {
errorIcon()
@ -129,8 +129,8 @@ private inline fun DefaultCurrencyIcon(
iconData: Any,
alpha: Float,
colorFilter: ColorFilter?,
crossinline errorIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
crossinline errorIcon: @Composable () -> Unit,
) {
var iconBackgroundColor by remember { mutableStateOf(Color.Transparent) }
var isBackgroundColorDefined by remember { mutableStateOf(false) }

View file

@ -1,6 +1,6 @@
package com.tangem.features.txhistory.utils
internal interface TxHistoryUiActions {
interface TxHistoryUiActions {
fun openExplorer()
fun openTxInExplorer(txHash: String)

View file

@ -52,8 +52,8 @@ import com.tangem.feature.walletsettings.impl.R
@Composable
internal fun WalletSettingsScreen(
state: WalletSettingsUM,
dialog: @Composable () -> Unit,
modifier: Modifier = Modifier,
dialog: @Composable () -> Unit,
) {
val backgroundColor = TangemTheme.colors.background.secondary

View file

@ -115,6 +115,7 @@ dependencies {
implementation(projects.features.sendV2.api)
implementation(projects.features.kyc.api)
implementation(projects.features.tokenRecieve.api)
implementation(projects.features.yieldSupply.api)
/** Common modules */
implementation(projects.common)

View file

@ -66,6 +66,7 @@ internal class WalletWarningsAnalyticsSender @Inject constructor(
is WalletNotification.UsedOutdatedData,
is WalletNotification.UnlockVisaAccess,
is WalletNotification.FinishWalletActivation,
is WalletNotification.Warning.YeildSupplyApprove, // TODO apply correct event
-> null
is WalletNotification.Critical.SeedPhraseNotification -> MainScreen.NoticeSeedPhraseSupport
is WalletNotification.Critical.SeedPhraseSecondNotification -> MainScreen.NoticeSeedPhraseSupportSecond

View file

@ -31,8 +31,10 @@ import com.tangem.domain.wallets.usecase.SeedPhraseNotificationUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.impl.R
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.features.yield.supply.api.YieldSupplyFeatureToggles
import com.tangem.lib.crypto.BlockchainUtils.isBitcoin
import com.tangem.utils.coroutines.combine6
import com.tangem.utils.extensions.addIf
import com.tangem.utils.extensions.isPositive
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@ -53,6 +55,7 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
private val getCryptoCurrenciesUseCase: GetCryptoCurrenciesUseCase,
private val onrampSepaAvailableUseCase: OnrampSepaAvailableUseCase,
private val getOnrampCountryUseCase: GetOnrampCountryUseCase,
private val yieldSupplyFeatureToggles: YieldSupplyFeatureToggles,
) {
fun create(userWallet: UserWallet, clickIntents: WalletClickIntents): Flow<ImmutableList<WalletNotification>> {
@ -81,6 +84,8 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
addWarningNotifications(cardTypesResolver, maybeTokenList, isNeedToBackup, clickIntents)
addYieldSupplyNotifications(maybeTokenList)
val hasCriticalOrWarning = any { notification ->
notification is WalletNotification.Critical || notification is WalletNotification.Warning
}
@ -267,6 +272,15 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun MutableList<WalletNotification>.addYieldSupplyNotifications(
tokenList: Lce<TokenListError, TokenList>,
) {
addIf(
element = WalletNotification.Warning.YeildSupplyApprove,
condition = tokenList.hasTokensWithActivatedSupplyWithoutApprove(),
)
}
private fun MutableList<WalletNotification>.addWarningNotifications(
cardTypesResolver: CardTypesResolver?,
tokenList: Lce<TokenListError, TokenList>,
@ -297,6 +311,14 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
return tokenList.flattenCurrencies().any { it.value is CryptoCurrencyStatus.Unreachable }
}
private fun Lce<TokenListError, TokenList>.hasTokensWithActivatedSupplyWithoutApprove(): Boolean {
val tokenList = getOrNull(isPartialContentAccepted = false) ?: return false
val yieldSupplyEnabled = yieldSupplyFeatureToggles.isYieldSupplyFeatureEnabled
return yieldSupplyEnabled && tokenList.flattenCurrencies().any {
it.value.yieldSupplyStatus?.isAllowedToSpend == false
}
}
private fun MutableList<WalletNotification>.addRateTheAppNotification(
isReadyToShowRating: Boolean,
clickIntents: WalletClickIntents,
@ -356,10 +378,6 @@ internal class GetMultiWalletWarningsFactory @Inject constructor(
)
}
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
if (condition) add(element = element)
}
private companion object {
const val MAX_REMAINING_SIGNATURES_COUNT = 10
}

View file

@ -15,6 +15,7 @@ import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.IsNeedToBackupUseCase
import com.tangem.feature.wallet.child.wallet.model.intents.WalletClickIntents
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletNotification
import com.tangem.utils.extensions.addIf
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.*
@ -204,14 +205,15 @@ internal class GetSingleWalletWarningsFactory @Inject constructor(
}
private fun MutableList<WalletNotification>.addIf(element: WalletNotification, condition: Boolean) {
if (condition) {
add(element = element)
addIf(condition) {
if (element is WalletNotification.Critical ||
element is WalletNotification.Warning ||
element is WalletNotification.NoteMigration
) {
readyForRateAppNotification = false
}
element
}
}

View file

@ -138,6 +138,11 @@ sealed class WalletNotification(val config: NotificationConfig) {
formatArgs = wrappedList(count),
),
)
data object YeildSupplyApprove : Warning(
title = resourceReference(R.string.yield_module_main_view_approve_notification_title),
subtitle = resourceReference(R.string.yield_module_main_view_approve_notification_description),
)
}
sealed class Informational(

View file

@ -59,8 +59,8 @@ internal class SetTxHistoryCountTransformer(
private fun createLoadingItems(): List<TxHistoryState.TxHistoryItemState> {
return buildList {
add(TxHistoryState.TxHistoryItemState.Title(onExploreClick = clickIntents::onExploreClick))
(1..transactionsCount).forEach {
add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(it.toString())))
for (i in 1..transactionsCount) {
add(TxHistoryState.TxHistoryItemState.Transaction(state = TransactionState.Loading(i.toString())))
}
}
}

View file

@ -291,10 +291,10 @@ private inline fun BaseScaffoldWithMarkets(
listState: LazyListState,
selectedWallet: WalletState,
snackbarHostState: SnackbarHostState,
bottomSheetHeaderHeightProvider: () -> Dp,
crossinline bottomSheetContent: @Composable () -> Unit,
alertConfig: WalletAlertState?,
bottomSheetHeaderHeightProvider: () -> Dp,
noinline onBottomSheetStateChange: (BottomSheetState) -> Unit,
crossinline bottomSheetContent: @Composable () -> Unit,
crossinline content: @Composable (PaddingValues) -> Unit,
) {
val bottomSheetState = rememberTangemStandardBottomSheetState()

View file

@ -68,11 +68,11 @@ private fun BalancesAndLimitsBlock(state: BalancesAndLimitsBlockState, modifier:
@Composable
private inline fun ContentContainer(
enabled: Boolean,
modifier: Modifier = Modifier,
noinline onClick: () -> Unit,
crossinline title: @Composable () -> Unit,
crossinline content: @Composable () -> Unit,
crossinline endIcon: @Composable () -> Unit,
modifier: Modifier = Modifier,
) {
Card(
modifier = modifier.fillMaxWidth(),

View file

@ -133,10 +133,10 @@ private fun InfoButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
@Composable
private inline fun ContentContainer(
modifier: Modifier = Modifier,
title: @Composable BoxScope.() -> Unit,
firstBlock: @Composable ColumnScope.() -> Unit,
secondBlock: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.background(TangemTheme.colors.background.secondary),

View file

@ -19,8 +19,8 @@ private const val BLOCK_ITEM_VALUE_WEIGHT = .55f
@Composable
internal inline fun BlockContent(
title: TextReference,
content: @Composable ColumnScope.() -> Unit,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit,
description: @Composable RowScope.() -> Unit = {},
) {
Column(

View file

@ -45,7 +45,7 @@ internal class WcConnectedAppInfoModel @Inject constructor(
private val params = paramsContainer.require<WcConnectedAppInfoContainerComponent.Params>()
val uiState: StateFlow<WcConnectedAppInfoUM?>
field = MutableStateFlow<WcConnectedAppInfoUM?>(null)
field = MutableStateFlow<WcConnectedAppInfoUM?>(null)
val stackNavigation = StackNavigation<ConnectedAppInfoRoutes>()

View file

@ -55,7 +55,7 @@ internal class WcConnectionsModel @Inject constructor(
private val params = paramsContainer.require<WcConnectionsComponent.Params>()
val uiState: StateFlow<WcConnectionsState>
field = MutableStateFlow<WcConnectionsState>(getInitialState())
field = MutableStateFlow<WcConnectionsState>(getInitialState())
val bottomSheetNavigation: SlotNavigation<WcConnectionsBottomSheetConfig> = SlotNavigation()
init {

View file

@ -80,7 +80,7 @@ internal class WcPairModel @Inject constructor(
private val dAppVerifiedStateConverter = WcDAppVerifiedStateConverter(onVerifiedClick = ::showVerifiedAlert)
val appInfoUiState: StateFlow<WcAppInfoUM>
field = MutableStateFlow<WcAppInfoUM>(createLoadingState())
field = MutableStateFlow<WcAppInfoUM>(createLoadingState())
init {
loadDAppInfo()

View file

@ -37,7 +37,7 @@ internal class WcSelectNetworksModel @Inject constructor(
)
val state: StateFlow<WcSelectNetworksUM>
field = MutableStateFlow(getInitialState())
field = MutableStateFlow(getInitialState())
private fun onCheckedChange(isChecked: Boolean, network: Network) {
additionallyEnabledNetworks.update {

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