Updated on 2026-08-14
This commit is contained in:
commit
7db00540c5
351 changed files with 6478 additions and 1691 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = "",
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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?) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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() }
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,5 +9,4 @@ internal enum class OnrampOperation {
|
|||
BUY,
|
||||
SELL,
|
||||
SWAP,
|
||||
;
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ internal class TangemPayDetailsModel @Inject constructor(
|
|||
) : Model() {
|
||||
|
||||
val uiState: StateFlow<TangemPayDetailsUM>
|
||||
field = MutableStateFlow(getInitialState())
|
||||
field = MutableStateFlow(getInitialState())
|
||||
|
||||
private val refreshStateJobHolder = JobHolder()
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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(), {})
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -23,4 +23,5 @@ internal data class TokenDetailsState(
|
|||
val bottomSheetConfig: TangemBottomSheetConfig?,
|
||||
val isBalanceHidden: Boolean,
|
||||
val isMarketPriceAvailable: Boolean,
|
||||
val isYieldSupplyFeatureEnabled: Boolean,
|
||||
)
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
)
|
||||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package com.tangem.features.txhistory.utils
|
||||
|
||||
internal interface TxHistoryUiActions {
|
||||
interface TxHistoryUiActions {
|
||||
|
||||
fun openExplorer()
|
||||
fun openTxInExplorer(txHash: String)
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue