Updated on 2026-08-14

This commit is contained in:
Tangem 2025-07-23 14:37:50 +03:00
commit 2f84def7cc
1244 changed files with 20573 additions and 7307 deletions

View file

@ -25,7 +25,7 @@ dependencies {
/** Domain */
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
api(projects.domain.models)
implementation(projects.domain.settings)
implementation(projects.domain.card)

View file

@ -13,10 +13,10 @@ import com.tangem.core.ui.message.DialogMessage
import com.tangem.core.ui.message.EventMessageAction
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.settings.SetSaveWalletScreenShownUseCase
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.biometry.impl.ui.state.AskBiometryUM

View file

@ -155,7 +155,7 @@ private fun WalletBlock(
Row {
Text(
modifier = Modifier
.weight(1f)
.weight(1f, fill = false)
.padding(end = 8.dp),
text = title,
style = TangemTheme.typography.subtitle1,

View file

@ -11,7 +11,7 @@ android {
dependencies {
/* Project - Domain */
implementation(projects.domain.wallets.models)
implementation(projects.domain.models)
/* Project - Core */
implementation(projects.core.decompose)

View file

@ -2,13 +2,11 @@ package com.tangem.features.details.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
interface DetailsComponent : ComposableContentComponent {
interface Factory : ComponentFactory<Params, DetailsComponent>
data class Params(
val userWalletId: UserWalletId,
)
data class Params(val userWalletId: UserWalletId)
}

View file

@ -5,7 +5,7 @@ import androidx.compose.ui.Modifier
import com.tangem.core.decompose.navigation.DummyRouter
import com.tangem.core.navigation.url.DummyUrlOpener
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.details.component.DetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsUM

View file

@ -10,7 +10,7 @@ import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.wrappedList
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.details.component.UserWalletListComponent
import com.tangem.features.details.entity.UserWalletListUM
import com.tangem.features.details.impl.R

View file

@ -19,8 +19,8 @@ import com.tangem.domain.feedback.repository.FeedbackFeatureToggles
import com.tangem.domain.redux.LegacyAction
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.walletconnect.CheckIsWalletConnectAvailableUseCase
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.requireColdWallet
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.wallets.usecase.GetSelectedWalletSyncUseCase
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.features.details.component.DetailsComponent

View file

@ -25,7 +25,7 @@ import com.tangem.core.ui.decompose.ComposableContentComponent
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.TestTags
import com.tangem.core.ui.test.DetailsScreenTestTags
import com.tangem.features.details.component.preview.PreviewDetailsComponent
import com.tangem.features.details.entity.DetailsFooterUM
import com.tangem.features.details.entity.DetailsItemUM
@ -67,7 +67,7 @@ private fun Content(
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier.testTag(TestTags.DETAILS_SCREEN),
modifier = modifier.testTag(DetailsScreenTestTags.SCREEN_CONTAINER),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing16),
contentPadding = PaddingValues(
top = TangemTheme.dimens.spacing12,
@ -120,7 +120,7 @@ private fun Block(
) {
val itemModifier = Modifier
.fillMaxWidth()
.testTag(TestTags.DETAILS_SCREEN_ITEM)
.testTag(DetailsScreenTestTags.SCREEN_ITEM)
when (model) {
is DetailsItemUM.Basic -> {

View file

@ -5,10 +5,8 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.ui.components.block.model.BlockUM
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.details.entity.DetailsItemUM
import com.tangem.features.details.impl.BuildConfig
import com.tangem.features.details.impl.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -76,17 +74,6 @@ internal class ItemsBuilder @Inject constructor(private val router: Router) {
onClick = { router.push(AppRoute.AppSettings) },
),
).let(::add)
if (BuildConfig.TESTER_MENU_ENABLED) {
DetailsItemUM.Basic.Item(
id = "tester_menu",
block = BlockUM(
text = stringReference(value = "Tester menu"),
iconRes = R.drawable.ic_alert_24,
onClick = { router.push(AppRoute.TesterMenu) },
),
).let(::add)
}
}.toImmutableList(),
)

View file

@ -22,7 +22,7 @@ import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.redux.ReduxStateHolder
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.models.SaveWalletError
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.ShouldSaveUserWalletsSyncUseCase
import com.tangem.features.details.impl.R

View file

@ -32,8 +32,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_ACCEPT_BUTTON
import com.tangem.core.ui.test.TestTags.DISCLAIMER_SCREEN_CONTAINER
import com.tangem.core.ui.test.DisclaimerScreenTestTags
import com.tangem.core.ui.webview.applySafeSettings
import com.tangem.features.disclaimer.impl.R
import com.tangem.features.disclaimer.impl.entity.DisclaimerUM
@ -58,7 +57,7 @@ internal fun DisclaimerScreen(state: DisclaimerUM) {
modifier = Modifier
.background(backgroundColor)
.statusBarsPadding()
.testTag(DISCLAIMER_SCREEN_CONTAINER),
.testTag(DisclaimerScreenTestTags.SCREEN_CONTAINER),
) {
Column(
modifier = Modifier
@ -183,7 +182,7 @@ private fun BoxScope.DisclaimerButton(onAccept: (Boolean) -> Unit) {
disabledContentColor = TangemColorPalette.Dark6,
),
modifier = Modifier
.testTag(DISCLAIMER_SCREEN_ACCEPT_BUTTON)
.testTag(DisclaimerScreenTestTags.ACCEPT_BUTTON)
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.padding(

View file

@ -12,11 +12,18 @@ dependencies {
/* Project - Domain */
implementation(projects.domain.models)
implementation(projects.domain.wallets.models)
/* Project - Core */
implementation(projects.core.decompose)
implementation(projects.core.ui)
/* Tangem libraries */
implementation(tangemDeps.card.core)
/* Compose */
implementation(deps.compose.runtime)
/* Tangem libs */
implementation(tangemDeps.hot.core)
}

View file

@ -0,0 +1,9 @@
package com.tangem.features.hotwallet
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
interface HotAccessCodeRequestComponent : ComposableContentComponent, HotWalletPasswordRequester {
interface Factory : ComponentFactory<Unit, HotAccessCodeRequestComponent>
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.hotwallet
import com.tangem.hot.sdk.model.HotAuth
interface HotWalletPasswordRequester {
suspend fun wrongPassword()
suspend fun requestPassword(hasBiometry: Boolean): Result
suspend fun dismiss()
sealed class Result {
data object UseBiometry : Result()
data object Dismiss : Result()
data class EnteredPassword(val password: HotAuth.Password) : Result()
}
}

View file

@ -0,0 +1,16 @@
package com.tangem.features.hotwallet
import com.tangem.crypto.bip39.Mnemonic
interface MnemonicRepository {
val words: Set<String>
fun generateMnemonic(type: MnemonicType = MnemonicType.Words12): Mnemonic
fun generateMnemonic(mnemonicString: String): Mnemonic
enum class MnemonicType {
Words12, Words24,
}
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.hotwallet
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.wallet.UserWalletId
interface WalletBackupComponent : ComposableContentComponent {
data class Params(
val userWalletId: UserWalletId,
)
interface Factory : ComponentFactory<Params, WalletBackupComponent>
}

View file

@ -14,6 +14,7 @@ android {
dependencies {
/** Api */
implementation(projects.features.hotWallet.api)
implementation(projects.features.pushNotifications.api)
/** Core modules */
implementation(projects.core.configToggles)
@ -26,6 +27,12 @@ dependencies {
implementation(projects.core.navigation)
implementation(projects.core.datasource)
/** Domain */
implementation(projects.domain.models)
implementation(projects.domain.wallets)
implementation(projects.domain.wallets.models)
implementation(projects.domain.models)
/** Common */
implementation(projects.common.ui)
implementation(projects.common.routing)
@ -36,6 +43,8 @@ dependencies {
implementation(tangemDeps.card.android) {
exclude(module = "joda-time")
}
implementation(tangemDeps.hot.core)
implementation(tangemDeps.hot.android)
/** AndroidX libraries */
implementation(deps.androidx.core.ktx)

View file

@ -0,0 +1,70 @@
package com.tangem.features.hotwallet.accesscoderequest
import androidx.compose.foundation.focusable
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.components.FullScreen
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.HotWalletPasswordRequester
import com.tangem.features.hotwallet.accesscoderequest.ui.HotAccessCodeRequestFullScreenContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
internal class DefaultHotAccessCodeRequestComponent @AssistedInject constructor(
@Assisted appComponentContext: AppComponentContext,
@Assisted params: Unit,
) : AppComponentContext by appComponentContext, HotAccessCodeRequestComponent {
private val model: HotAccessCodeRequestModel = getOrCreateModel(params)
override suspend fun wrongPassword() {
model.wrongAccessCode()
}
override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result {
model.show(hasBiometry)
return model.waitResult()
}
override suspend fun dismiss() {
model.dismiss()
}
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
var isShownProxy by remember { mutableStateOf(state.isShown) }
var isShownIfProxy by remember { mutableStateOf(state.isShown) }
if (isShownIfProxy) {
FullScreen(focusable = true, onBackClick = state.onDismiss) {
HotAccessCodeRequestFullScreenContent(
state = state.copy(isShown = isShownProxy),
modifier = modifier,
)
}
}
LaunchedEffect(state.isShown) {
if (state.isShown) {
isShownIfProxy = true
delay(timeMillis = 100)
isShownProxy = true
} else {
isShownProxy = false
delay(timeMillis = 250)
isShownIfProxy = false
}
}
}
@AssistedFactory
interface Factory : HotAccessCodeRequestComponent.Factory {
override fun create(context: AppComponentContext, params: Unit): DefaultHotAccessCodeRequestComponent
}
}

View file

@ -0,0 +1,89 @@
package com.tangem.features.hotwallet.accesscoderequest
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.HotWalletPasswordRequester
import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
internal class HotAccessCodeRequestModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val result = MutableStateFlow<HotWalletPasswordRequester.Result?>(null)
val uiState: StateFlow<HotAccessCodeRequestUM>
field = MutableStateFlow(getInitialState())
fun dismiss() {
result.value = HotWalletPasswordRequester.Result.Dismiss
dismissState()
}
fun show(hasBiometry: Boolean) {
result.value = null // Reset the result when showing the dialog
uiState.update {
it.copy(
isShown = true,
accessCode = "",
useBiometricVisible = hasBiometry,
onAccessCodeChange = ::onAccessCodeChange,
)
}
}
suspend fun waitResult(): HotWalletPasswordRequester.Result {
return result.filterNotNull().first().also { result.value = null }
}
suspend fun wrongAccessCode() {
uiState.update {
it.copy(
wrongAccessCode = true,
onAccessCodeChange = {},
)
}
delay(timeMillis = 500) // Delay to show the wrong access code state
}
private fun getInitialState() = HotAccessCodeRequestUM(
onDismiss = ::dismiss,
onAccessCodeChange = ::onAccessCodeChange,
accessCode = "",
useBiometricClick = {
dismissState()
result.value = HotWalletPasswordRequester.Result.UseBiometry
},
)
private fun onAccessCodeChange(accessCode: String) {
if (accessCode.length > ACCESS_CODE_LENGTH) return
uiState.update {
it.copy(accessCode = accessCode, wrongAccessCode = false)
}
if (accessCode.length == ACCESS_CODE_LENGTH) {
uiState.update {
it.copy(onAccessCodeChange = {})
}
result.value = HotWalletPasswordRequester.Result.EnteredPassword(HotAuth.Password(accessCode.toCharArray()))
}
}
private fun dismissState() {
uiState.update {
it.copy(isShown = false)
}
}
private companion object {
const val ACCESS_CODE_LENGTH = 6
}
}

View file

@ -0,0 +1,32 @@
package com.tangem.features.hotwallet.accesscoderequest.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.HotAccessCodeRequestComponent
import com.tangem.features.hotwallet.HotWalletPasswordRequester
import com.tangem.features.hotwallet.accesscoderequest.DefaultHotAccessCodeRequestComponent
import com.tangem.features.hotwallet.accesscoderequest.HotAccessCodeRequestModel
import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface ComponentModuleBinds {
@Binds
fun bindComponentFactory(impl: DefaultHotAccessCodeRequestComponent.Factory): HotAccessCodeRequestComponent.Factory
@Binds
@IntoMap
@ClassKey(HotAccessCodeRequestModel::class)
fun bindCreateMobileWalletModel(model: HotAccessCodeRequestModel): Model
@Binds
@Singleton
fun bindHotWalletPasswordRequester(impl: HotWalletPasswordRequesterProxy): HotWalletPasswordRequester
}

View file

@ -0,0 +1,11 @@
package com.tangem.features.hotwallet.accesscoderequest.entity
internal data class HotAccessCodeRequestUM(
val isShown: Boolean = false,
val accessCode: String = "",
val wrongAccessCode: Boolean = false,
val useBiometricVisible: Boolean = true,
val useBiometricClick: () -> Unit = {},
val onAccessCodeChange: (String) -> Unit = {},
val onDismiss: () -> Unit = {},
)

View file

@ -0,0 +1,32 @@
package com.tangem.features.hotwallet.accesscoderequest.proxy
import com.tangem.features.hotwallet.HotWalletPasswordRequester
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withTimeout
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class HotWalletPasswordRequesterProxy @Inject constructor() : HotWalletPasswordRequester {
val componentRequester = MutableStateFlow<HotWalletPasswordRequester?>(null)
override suspend fun wrongPassword() {
call { wrongPassword() }
}
override suspend fun requestPassword(hasBiometry: Boolean): HotWalletPasswordRequester.Result =
call { requestPassword(hasBiometry) }
override suspend fun dismiss() {
call { dismiss() }
}
private suspend fun <T> call(block: suspend HotWalletPasswordRequester.() -> T): T {
return withTimeout(timeMillis = 1000) {
componentRequester.filterNotNull().first()
}.block()
}
}

View file

@ -0,0 +1,137 @@
package com.tangem.features.hotwallet.accesscoderequest.ui
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.SecondaryButton
import com.tangem.core.ui.components.SpacerH
import com.tangem.core.ui.components.SpacerH24
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.fields.PinTextField
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.haptic.TangemHapticEffect
import com.tangem.core.ui.res.LocalHapticManager
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.accesscoderequest.entity.HotAccessCodeRequestUM
import com.tangem.features.hotwallet.impl.R
@Suppress("MagicNumber", "LongMethod")
@Composable
internal fun HotAccessCodeRequestFullScreenContent(state: HotAccessCodeRequestUM, modifier: Modifier = Modifier) {
AnimatedVisibility(
modifier = modifier,
visible = state.isShown,
enter = fadeIn(),
exit = fadeOut(),
) {
Column(
Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary),
horizontalAlignment = Alignment.CenterHorizontally,
) {
TangemTopAppBar(
modifier = Modifier
.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onDismiss),
)
SpacerH(68.dp)
Column(
Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
text = stringResourceSafe(R.string.access_code_check_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
SpacerH24()
PinTextField(
modifier = Modifier.animateEnterExit(
enter = slideInVertically(
tween(),
initialOffsetY = { it + 200 },
) + fadeIn(tween()),
exit = slideOutVertically(tween(300)) { it - 200 } + fadeOut(tween()),
),
length = 6,
isPasswordVisual = true,
value = state.accessCode,
wrongCode = state.wrongAccessCode,
onValueChange = state.onAccessCodeChange,
)
}
if (state.useBiometricVisible) {
SecondaryButton(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth()
.navigationBarsPadding()
.imePadding(),
text = "Use biometric",
onClick = state.useBiometricClick,
)
}
}
}
val hapticManager = LocalHapticManager.current
LaunchedEffect(state.wrongAccessCode) {
if (state.wrongAccessCode) {
hapticManager.perform(TangemHapticEffect.View.Reject)
}
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
Box(
Modifier
.fillMaxSize()
.background(TangemTheme.colors.background.secondary),
) {
var isShown by remember { mutableStateOf(true) }
HotAccessCodeRequestFullScreenContent(
state = HotAccessCodeRequestUM(isShown = isShown),
modifier = Modifier,
)
Button(onClick = { isShown = !isShown }) {
Text("TOggle")
}
}
}
}

View file

@ -7,6 +7,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.decompose.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject

View file

@ -1,5 +1,23 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.entity
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
internal data class AddExistingWalletImportUM(
val onBackClick: () -> Unit,
val words: TextFieldValue,
val wordsChange: (TextFieldValue) -> Unit,
val passPhrase: TextFieldValue,
val passPhraseChange: (TextFieldValue) -> Unit,
val onPassphraseInfoClick: () -> Unit,
val wordsErrorText: TextReference?,
val invalidWords: ImmutableList<String>,
val createWalletEnabled: Boolean,
val createWalletProgress: Boolean,
val createWalletClick: () -> Unit,
val suggestionsList: ImmutableList<String>,
val onSuggestionClick: (String) -> Unit,
val infoBottomSheetConfig: TangemBottomSheetConfig,
val readyToImport: Boolean,
)

View file

@ -0,0 +1,44 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.model
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@ModelScoped
internal class AddExistingWalletImportModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val mnemonicRepository: MnemonicRepository,
) : Model() {
private val importSeedPhraseUiStateBuilder: ImportSeedPhraseUiStateBuilder
init {
importSeedPhraseUiStateBuilder = ImportSeedPhraseUiStateBuilder(
modelScope = modelScope,
mnemonicRepository = mnemonicRepository,
updateUiState = { block -> uiState.update { block(it) } },
readyToImport = { ready -> uiState.update { it.copy(readyToImport = ready) } },
importWallet = { mnemonic: Mnemonic, passphrase: String? ->
importWallet(
mnemonic = mnemonic,
passphrase = passphrase,
)
},
)
}
internal val uiState: StateFlow<AddExistingWalletImportUM>
field = MutableStateFlow(importSeedPhraseUiStateBuilder.getState())
@Suppress("UnusedPrivateMember")
private fun importWallet(mnemonic: Mnemonic, passphrase: String?) {
// TODO implement importing seed phrase
}
}

View file

@ -0,0 +1,188 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.model
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.common.core.TangemSdkError
import com.tangem.core.ui.R
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.MnemonicErrorResult
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
internal class ImportSeedPhraseUiStateBuilder(
private val modelScope: CoroutineScope,
private val mnemonicRepository: MnemonicRepository,
private val readyToImport: (Boolean) -> Unit,
private val updateUiState: ((AddExistingWalletImportUM) -> AddExistingWalletImportUM) -> Unit,
private val importWallet: (mnemonic: Mnemonic, passphrase: String?) -> Unit,
) {
private val wordsCheckJobHolder = JobHolder()
private var importedMnemonic: Mnemonic? = null
private var passphrase: String? = null
fun getState(): AddExistingWalletImportUM {
return AddExistingWalletImportUM(
words = TextFieldValue(""),
passPhrase = TextFieldValue(""),
wordsErrorText = null,
invalidWords = persistentListOf(),
createWalletEnabled = false,
createWalletProgress = false,
suggestionsList = persistentListOf(),
wordsChange = {
launchInterceptWords(wordsField = it)
suggestNextWord(it)
updateUiState { state ->
state.copy(words = it)
}
},
passPhraseChange = {
passphrase = it.text
updateUiState { state -> state.copy(passPhrase = it) }
},
onPassphraseInfoClick = ::showInfoBS,
createWalletClick = ::onCreateWallet,
onSuggestionClick = { word -> addSuggestedWord(word) },
readyToImport = false,
infoBottomSheetConfig = TangemBottomSheetConfig.Empty,
)
}
private fun onCreateWallet() {
val mnemonic = importedMnemonic ?: return
val passphrase = passphrase?.takeIf { it.isNotEmpty() }
importWallet(mnemonic, passphrase)
}
private fun addSuggestedWord(word: String) {
updateUiState { st ->
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),
),
)
}
}
private fun suggestNextWord(wordsField: TextFieldValue) {
if (wordsField.text.endsWith(" ")) {
updateUiState { it.copy(suggestionsList = emptyList<String>().toImmutableList()) }
return
}
val text = wordsField.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val lastWord = wordsFromText.lastOrNull() ?: return
if (lastWord.length < MINIMUM_WORD_LENGTH) return
if (mnemonicRepository.words.contains(lastWord) && mnemonicRepository.words.none { it.startsWith(lastWord) }) {
updateUiState { it.copy(suggestionsList = emptyList<String>().toImmutableList()) }
return
}
updateUiState { st ->
st.copy(
suggestionsList = mnemonicRepository.words
.filter { it.startsWith(lastWord) && it != lastWord }
.toImmutableList(),
)
}
}
private fun launchInterceptWords(wordsField: TextFieldValue) {
modelScope.launch {
delay(timeMillis = WORDS_INTERCEPT_DELAY_MS)
interceptWords(wordsField)
}.saveIn(wordsCheckJobHolder)
}
private fun interceptWords(wordsField: TextFieldValue) {
readyToImport(false)
updateUiState {
it.copy(
createWalletEnabled = false,
wordsErrorText = null,
)
}
importedMnemonic = null
val text = wordsField.text
val wordsFromText = text.split(" ").filter { it.isNotBlank() }.map { it.trim() }
val invalidWords = wordsFromText.filterNot { it in mnemonicRepository.words }
if (invalidWords.isNotEmpty()) {
updateUiState {
it.copy(
invalidWords = invalidWords.toImmutableList(),
wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_wrong_words),
createWalletEnabled = false,
)
}
return
}
try {
val mnemonic = mnemonicRepository.generateMnemonic(text)
importedMnemonic = mnemonic
updateUiState {
it.copy(
invalidWords = emptyList<String>().toImmutableList(),
wordsErrorText = null,
createWalletEnabled = true,
)
}
readyToImport(true)
} catch (ex: TangemSdkError.MnemonicException) {
readyToImport(false)
val error = ex.mnemonicResult
if (error is MnemonicErrorResult.InvalidChecksum) {
updateUiState {
it.copy(
wordsErrorText = resourceReference(R.string.onboarding_seed_mnemonic_invalid_checksum),
createWalletEnabled = false,
)
}
} else {
updateUiState {
it.copy(
createWalletEnabled = false,
wordsErrorText = null,
)
}
}
}
}
private fun showInfoBS() {
updateUiState { state ->
state.copy(
infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty.copy(
isShown = true,
onDismissRequest = {
updateUiState { it.copy(infoBottomSheetConfig = TangemBottomSheetConfig.Companion.Empty) }
},
),
)
}
}
companion object {
private const val MINIMUM_WORD_LENGTH = 2
private const val WORDS_INTERCEPT_DELAY_MS = 500L
}
}

View file

@ -1,36 +1,218 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideIn
import androidx.compose.animation.slideOut
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
import com.tangem.core.ui.R
import com.tangem.core.ui.components.Keyboard
import com.tangem.core.ui.components.Notifier
import com.tangem.core.ui.components.OutlineTextFieldWithIcon
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.TangemTextFieldsDefault
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.keyboardAsState
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils.InvalidWordsColorTransformation
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.systemBarsPadding(),
) {
TangemTopAppBar(
Box(modifier.fillMaxSize()) {
Column(
modifier = Modifier
.statusBarsPadding(),
startButton = TopAppBarButtonUM.Back(state.onBackClick),
title = stringResourceSafe(R.string.wallet_import_title),
.fillMaxSize()
.imePadding(),
) {
Column(
Modifier
.verticalScroll(rememberScrollState())
.weight(1f),
) {
Text(
text = stringResourceSafe(id = R.string.onboarding_seed_import_message),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(start = 36.dp, end = 36.dp, top = 24.dp, bottom = 16.dp)
.fillMaxWidth(),
)
PhraseBlock(
state = state,
modifier = Modifier.padding(horizontal = 16.dp),
)
OutlineTextFieldWithIcon(
modifier = modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
value = state.passPhrase,
onValueChange = state.passPhraseChange,
iconResId = R.drawable.ic_information_24,
iconColor = TangemTheme.colors.icon.informative,
label = stringResourceSafe(id = R.string.common_passphrase),
placeholder = stringResourceSafe(id = R.string.send_optional_field),
onIconClick = state.onPassphraseInfoClick,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
}
PrimaryButtonIconEnd(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_import),
iconResId = R.drawable.ic_tangem_24,
enabled = state.createWalletEnabled,
showProgress = state.createWalletProgress,
onClick = state.createWalletClick,
)
}
val keyboard by keyboardAsState()
if (keyboard is Keyboard.Opened) {
SuggestionsBlock(
modifier = Modifier
.align(Alignment.BottomCenter)
.imePadding(),
suggestionsList = state.suggestionsList,
onClick = { index -> state.onSuggestionClick(state.suggestionsList[index]) },
)
}
}
PassphraseInfoBottomSheet(state.infoBottomSheetConfig)
}
@Composable
private fun PhraseBlock(state: AddExistingWalletImportUM, modifier: Modifier = Modifier) {
val warningColor = TangemTheme.colors.text.warning
val invalidWordsColorTransformation = remember(state.invalidWords, warningColor) {
InvalidWordsColorTransformation(
wordsToBrush = state.invalidWords,
style = SpanStyle(color = warningColor),
)
}
Column(
modifier = modifier.fillMaxWidth(),
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.height(TangemTheme.dimens.size142),
value = state.words,
onValueChange = state.wordsChange,
textStyle = TangemTheme.typography.body1,
singleLine = false,
colors = TangemTextFieldsDefault.defaultTextFieldColors,
visualTransformation = invalidWordsColorTransformation,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.height(TangemTheme.dimens.size32),
) {
if (state.wordsErrorText != null) {
Text(
modifier = Modifier.fillMaxSize(),
text = state.wordsErrorText.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.warning,
)
}
}
}
}
@Composable
private fun SuggestionsBlock(
suggestionsList: ImmutableList<String>,
onClick: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(
modifier = modifier,
enter = fadeIn() + slideIn(initialOffset = { IntOffset(x = 200, y = 0) }),
exit = slideOut(targetOffset = { IntOffset(x = -200, y = 0) }) + fadeOut(),
visible = suggestionsList.isNotEmpty(),
) {
LazyRow(
modifier = Modifier.padding(bottom = 8.dp),
contentPadding = PaddingValues(horizontal = TangemTheme.dimens.size16),
) {
items(suggestionsList.size) { index ->
SuggestionButton(
modifier = Modifier.rowPadding(
index = index,
rowSize = suggestionsList.size,
outSide = TangemTheme.dimens.size0,
inSide = TangemTheme.dimens.size4,
),
text = suggestionsList[index],
onClick = { onClick(index) },
)
}
}
}
}
@Composable
private fun SuggestionButton(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
Notifier(
text = text,
modifier = modifier
.clip(TangemTheme.shapes.roundedCorners8)
.clickable(onClick = onClick),
backgroundColor = TangemTheme.colors.icon.primary1,
textColor = TangemTheme.colors.text.primary2,
)
}
private fun Modifier.rowPadding(index: Int, rowSize: Int, outSide: Dp, inSide: Dp): Modifier = when (index) {
0 -> this.padding(start = outSide, end = inSide)
rowSize - 1 -> this.padding(start = inSide, end = outSide)
else -> this.padding(horizontal = inSide)
}
@Preview(showBackground = true, widthDp = 360)
@ -40,7 +222,20 @@ private fun PreviewAddExistingWalletImportContent() {
TangemThemePreview {
AddExistingWalletImportContent(
state = AddExistingWalletImportUM(
onBackClick = {},
words = TextFieldValue(""),
wordsChange = {},
passPhrase = TextFieldValue(""),
passPhraseChange = {},
onPassphraseInfoClick = {},
wordsErrorText = null,
invalidWords = persistentListOf(),
createWalletEnabled = false,
createWalletProgress = false,
createWalletClick = {},
suggestionsList = persistentListOf(),
onSuggestionClick = {},
infoBottomSheetConfig = TangemBottomSheetConfig.Empty,
readyToImport = false,
),
)
}

View file

@ -0,0 +1,90 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun PassphraseInfoBottomSheet(config: TangemBottomSheetConfig) {
TangemBottomSheet(
config = config,
containerColor = TangemTheme.colors.background.primary,
) { _: TangemBottomSheetConfigContent.Empty ->
PassphraseInfoBottomSheetContent(config.onDismissRequest)
}
}
@Composable
fun PassphraseInfoBottomSheetContent(onDismiss: () -> Unit) {
Column(
modifier = Modifier
.background(color = TangemTheme.colors.background.primary)
.fillMaxWidth(),
) {
Icon(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(top = TangemTheme.dimens.size40)
.size(TangemTheme.dimens.size48),
painter = painterResource(id = R.drawable.ic_information_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
Text(
text = stringResourceSafe(id = R.string.common_passphrase),
modifier = Modifier
.padding(top = TangemTheme.dimens.size40)
.align(Alignment.CenterHorizontally),
color = TangemTheme.colors.text.primary1,
style = TangemTheme.typography.h2,
)
Text(
text = stringResourceSafe(id = R.string.onboarding_bottom_sheet_passphrase_description),
modifier = Modifier
.padding(top = TangemTheme.dimens.size16)
.padding(horizontal = TangemTheme.dimens.size24)
.align(Alignment.CenterHorizontally),
color = TangemTheme.colors.text.secondary,
style = TangemTheme.typography.body2,
textAlign = TextAlign.Center,
)
PrimaryButton(
modifier = Modifier
.padding(horizontal = TangemTheme.dimens.size16)
.padding(top = TangemTheme.dimens.size40)
.padding(bottom = TangemTheme.dimens.size32)
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_ok),
onClick = onDismiss,
)
}
}
@Preview
@Composable
private fun PassphraseInfoBottomSheetContentPreview() {
TangemThemePreview {
PassphraseInfoBottomSheetContent({ })
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port.ui.utils
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import com.tangem.utils.StringsSigns.WHITE_SPACE
import com.tangem.utils.extensions.isSingleItem
import kotlinx.collections.immutable.ImmutableList
class InvalidWordsColorTransformation(
private val wordsToBrush: ImmutableList<String>,
private val style: SpanStyle,
) : VisualTransformation {
override fun filter(text: AnnotatedString): TransformedText {
val plainText = text.text
if (wordsToBrush.isSingleItem() && wordsToBrush.first() == plainText) {
return plainText.annotate().toTransformedText()
}
val wordList = makeSeparatedWordlist(plainText)
val annotatedString = makeAnnotatedString(wordList)
return annotatedString.toTransformedText()
}
private fun makeSeparatedWordlist(plainText: String): List<String> {
val plainTextCharArray = plainText.toCharArray()
val wordlist = mutableListOf<String>()
var wordBuilder = StringBuilder()
for (index in plainTextCharArray.indices) {
val char = plainTextCharArray[index]
if (char.isWhitespace()) {
if (wordBuilder.isEmpty()) {
wordlist.add(WORD_SEPARATOR)
} else {
wordlist.add(wordBuilder.toString())
wordBuilder = StringBuilder()
wordlist.add(WORD_SEPARATOR)
}
} else {
if (index == plainTextCharArray.lastIndex) {
wordBuilder.append(char)
wordlist.add(wordBuilder.toString())
wordBuilder = StringBuilder()
} else {
wordBuilder.append(char)
}
}
}
return wordlist.toList()
}
private fun makeAnnotatedString(wordList: List<String>): AnnotatedString = buildAnnotatedString {
wordList.forEach {
if (it == WORD_SEPARATOR) {
append(WHITE_SPACE)
} else if (wordsToBrush.contains(it)) {
append(it.annotate())
} else {
append(it)
}
}
}
private fun String.annotate(): AnnotatedString = AnnotatedString(this, style)
private fun AnnotatedString.toTransformedText(): TransformedText = TransformedText(this, OffsetMapping.Identity)
companion object {
private const val WORD_SEPARATOR = "_$@-*"
}
}

View file

@ -6,9 +6,11 @@ import com.arkivanov.decompose.router.stack.push
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.root.routing.AddExistingWalletRoute
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import javax.inject.Inject
@ -20,6 +22,8 @@ internal class AddExistingWalletModel @Inject constructor(
val addExistingWalletStartModelCallbacks = AddExistingWalletStartModelCallbacks()
val addExistingWalletImportModelCallbacks = AddExistingWalletImportModelCallbacks()
val pushNotificationsComponentModelCallbacks = PushNotificationsComponentModelCallbacks()
val accessCodeModelCallbacks = AccessCodeModelCallbacks()
val stackNavigation = StackNavigation<AddExistingWalletRoute>()
@ -38,4 +42,20 @@ internal class AddExistingWalletModel @Inject constructor(
stackNavigation.pop()
}
}
inner class PushNotificationsComponentModelCallbacks : PushNotificationsComponent.ModelCallbacks {
override fun onResult() {
// TODO [REDACTED_TASK_KEY]
}
}
inner class AccessCodeModelCallbacks : SetAccessCodeComponent.ModelCallbacks {
override fun onBackClick() {
// TODO [REDACTED_TASK_KEY]
}
override fun onAccessCodeSet() {
// TODO [REDACTED_TASK_KEY]
}
}
}

View file

@ -2,10 +2,11 @@ package com.tangem.features.hotwallet.addexistingwallet.root.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.AddExistingWalletComponent
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeModel
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.root.DefaultAddExistingWalletComponent
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -42,4 +43,9 @@ internal interface AddExistingWalletModuleBinds {
@IntoMap
@ClassKey(AddExistingWalletImportModel::class)
fun bindAddExistingWalletImportModel(model: AddExistingWalletImportModel): Model
@Binds
@IntoMap
@ClassKey(SetAccessCodeModel::class)
fun bindAccessCodeModel(model: SetAccessCodeModel): Model
}

View file

@ -2,12 +2,16 @@ package com.tangem.features.hotwallet.addexistingwallet.root.routing
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.setaccesscode.SetAccessCodeComponent
import com.tangem.features.hotwallet.addexistingwallet.root.AddExistingWalletModel
import com.tangem.features.hotwallet.addexistingwallet.start.AddExistingWalletStartComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.pushnotifications.api.PushNotificationsComponent
import javax.inject.Inject
internal class AddExistingWalletChildFactory @Inject constructor() {
internal class AddExistingWalletChildFactory @Inject constructor(
private val pushNotificationsComponent: PushNotificationsComponent.Factory,
) {
fun createChild(
route: AddExistingWalletRoute,
@ -27,6 +31,18 @@ internal class AddExistingWalletChildFactory @Inject constructor() {
callbacks = model.addExistingWalletImportModelCallbacks,
),
)
is AddExistingWalletRoute.PushNotifications -> pushNotificationsComponent.create(
context = childContext,
params = PushNotificationsComponent.Params.Callbacks(
callbacks = model.pushNotificationsComponentModelCallbacks,
),
)
is AddExistingWalletRoute.AccessCode -> SetAccessCodeComponent(
context = childContext,
params = SetAccessCodeComponent.Params(
callbacks = model.accessCodeModelCallbacks,
),
)
}
}
}

View file

@ -10,4 +10,10 @@ internal sealed class AddExistingWalletRoute : Route {
@Serializable
object Import : AddExistingWalletRoute()
@Serializable
object PushNotifications : AddExistingWalletRoute()
@Serializable
object AccessCode : AddExistingWalletRoute()
}

View file

@ -3,27 +3,27 @@ package com.tangem.features.hotwallet.addexistingwallet.start.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.appbar.TangemTopAppBar
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.buttons.common.TangemButtonSize
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.addexistingwallet.start.entity.AddExistingWalletStartUM
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.features.hotwallet.common.ui.OptionBlock
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@ -62,6 +62,7 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi
OptionBlock(
modifier = Modifier
.padding(top = 24.dp),
backgroundColor = TangemTheme.colors.background.secondary,
title = stringResourceSafe(R.string.wallet_import_seed_title),
description = stringResourceSafe(R.string.wallet_import_seed_description),
badge = null,
@ -69,6 +70,7 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi
enabled = true,
)
OptionBlock(
backgroundColor = TangemTheme.colors.background.secondary,
title = stringResourceSafe(R.string.wallet_import_scan_title),
description = stringResourceSafe(R.string.wallet_import_scan_description),
badge = {
@ -85,6 +87,7 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi
enabled = true,
)
OptionBlock(
backgroundColor = TangemTheme.colors.background.secondary,
title = stringResourceSafe(R.string.wallet_import_google_drive_title),
description = stringResourceSafe(R.string.wallet_import_google_drive_description),
badge = {
@ -114,58 +117,6 @@ internal fun AddExistingWalletStartContent(state: AddExistingWalletStartUM, modi
}
}
@Composable
private fun OptionBlock(
title: String,
description: String,
badge: (@Composable () -> Unit)?,
onClick: (() -> Unit)?,
enabled: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(top = 8.dp)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(
color = TangemTheme.colors.background.secondary,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.conditional(onClick != null) {
onClick?.let { clickableSingle(onClick = it) } ?: Modifier
}
.padding(16.dp),
) {
Row {
Text(
modifier = Modifier
.weight(1f, fill = false)
.padding(end = 4.dp),
text = title,
style = TangemTheme.typography.subtitle1,
color = if (enabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.secondary
},
)
badge?.invoke()
}
Text(
modifier = Modifier
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = if (enabled) {
TangemTheme.colors.text.tertiary
} else {
TangemTheme.colors.text.disabled
},
)
}
}
@Composable
private fun BuyTangemWalletBlock(onScanClick: () -> Unit, modifier: Modifier = Modifier) {
Row(

View file

@ -1,22 +1,24 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model
package com.tangem.features.hotwallet.common.repository
import android.content.Context
import com.tangem.crypto.bip39.DefaultMnemonic
import com.tangem.crypto.bip39.EntropyLength
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.crypto.bip39.Wordlist
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.MnemonicRepository.MnemonicType
import com.tangem.sdk.extensions.getWordlist
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
internal class MnemonicRepository @Inject constructor(
internal class DefaultMnemonicRepository @Inject constructor(
@ApplicationContext private val context: Context,
) {
) : MnemonicRepository {
private val wordlist = Wordlist.getWordlist(context)
val words: Set<String> = wordlist.words.toHashSet()
override val words: Set<String> = wordlist.words.toHashSet()
fun generateMnemonic(type: MnemonicType = MnemonicType.Words12): Mnemonic = DefaultMnemonic(
override fun generateMnemonic(type: MnemonicType): Mnemonic = DefaultMnemonic(
entropy = when (type) {
MnemonicType.Words12 -> EntropyLength.Bits128Length
MnemonicType.Words24 -> EntropyLength.Bits256Length
@ -24,12 +26,8 @@ internal class MnemonicRepository @Inject constructor(
wordlist = wordlist,
)
fun generateMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(
override fun generateMnemonic(mnemonicString: String): Mnemonic = DefaultMnemonic(
mnemonic = mnemonicString,
wordlist = wordlist,
)
enum class MnemonicType {
Words12, Words24,
}
}

View file

@ -0,0 +1,88 @@
package com.tangem.features.hotwallet.common.ui
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.res.TangemTheme
internal const val DISABLED_COLORS_ALPHA = 0.5f
@Suppress("LongParameterList")
@Composable
internal fun OptionBlock(
title: String,
description: String,
badge: (@Composable () -> Unit)?,
onClick: (() -> Unit)?,
enabled: Boolean,
backgroundColor: Color,
modifier: Modifier = Modifier,
) {
val backgroundColor by animateColorAsState(
targetValue = if (enabled) {
backgroundColor
} else {
backgroundColor.copy(alpha = DISABLED_COLORS_ALPHA)
},
)
val titleColor by animateColorAsState(
targetValue = if (enabled) {
TangemTheme.colors.text.primary1
} else {
TangemTheme.colors.text.primary1.copy(alpha = DISABLED_COLORS_ALPHA)
},
)
val descriptionColor by animateColorAsState(
targetValue = if (enabled) {
TangemTheme.colors.text.tertiary
} else {
TangemTheme.colors.text.tertiary.copy(alpha = DISABLED_COLORS_ALPHA)
},
)
Column(
modifier = modifier
.fillMaxWidth()
.padding(top = 8.dp)
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(
color = backgroundColor,
shape = TangemTheme.shapes.roundedCornersXMedium,
)
.conditional(onClick != null) {
onClick?.let { clickableSingle(onClick = it) } ?: Modifier
}
.padding(16.dp),
) {
Row {
Text(
modifier = Modifier
.weight(1f, fill = false)
.padding(end = 4.dp),
text = title,
style = TangemTheme.typography.subtitle1,
color = titleColor,
)
badge?.invoke()
}
Text(
modifier = Modifier
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = descriptionColor,
)
}
}

View file

@ -1,18 +1,30 @@
package com.tangem.features.hotwallet.createmobilewallet
import com.tangem.common.routing.AppRoute
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.createmobilewallet.entity.CreateMobileWalletUM
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.MnemonicType
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
internal class CreateMobileWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
private val saveUserWalletUseCase: SaveWalletUseCase,
private val router: Router,
private val tangemHotSdk: TangemHotSdk,
) : Model() {
internal val uiState: StateFlow<CreateMobileWalletUM>
@ -20,10 +32,30 @@ internal class CreateMobileWalletModel @Inject constructor(
CreateMobileWalletUM(
onBackClick = { router.pop() },
onCreateClick = ::onCreateClick,
createButtonLoading = false,
),
)
private fun onCreateClick() {
// TODO create a wallet
modelScope.launch {
uiState.update {
it.copy(createButtonLoading = true)
}
runCatching {
val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12)
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
saveUserWalletUseCase(
hotUserWalletBuilder.build(),
)
router.push(AppRoute.Wallet)
}.onFailure {
Timber.e(it)
uiState.update {
it.copy(createButtonLoading = false)
}
}
}
}
}

View file

@ -12,10 +12,12 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@Suppress("UnusedPrivateMember")
internal class DefaultCreateMobileWalletComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Unit,
) : CreateMobileWalletComponent, AppComponentContext by context {
private val model: CreateMobileWalletModel = getOrCreateModel(params)
@Composable

View file

@ -10,18 +10,12 @@ import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal object CreateMobileWalletModule
@Module
@InstallIn(SingletonComponent::class)
internal interface CreateMobileWalletModuleBinds {
@Binds
@Singleton
fun bindCreateMobileWalletComponentFactory(
impl: DefaultCreateMobileWalletComponent.Factory,
): CreateMobileWalletComponent.Factory

View file

@ -1,6 +1,7 @@
package com.tangem.features.hotwallet.createmobilewallet.entity
internal data class CreateMobileWalletUM(
val createButtonLoading: Boolean,
val onBackClick: () -> Unit,
val onCreateClick: () -> Unit,
)

View file

@ -84,8 +84,8 @@ internal fun CreateMobileWalletContent(state: CreateMobileWalletUM, modifier: Mo
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
text = "Create",
showProgress = false,
text = stringResourceSafe(R.string.common_create),
showProgress = state.createButtonLoading,
enabled = true,
onClick = state.onCreateClick,
)
@ -133,6 +133,7 @@ private fun PreviewCreateWalletContent() {
CreateMobileWalletContent(
state = CreateMobileWalletUM(
onBackClick = {},
createButtonLoading = false,
onCreateClick = {},
),
)

View file

@ -3,6 +3,9 @@ package com.tangem.features.hotwallet.di
import com.tangem.core.configtoggle.feature.FeatureTogglesManager
import com.tangem.features.hotwallet.DefaultHotWalletFeatureToggles
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.common.repository.DefaultMnemonicRepository
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -22,4 +25,8 @@ internal object HotWalletFeatureModule {
@Module
@InstallIn(SingletonComponent::class)
internal interface HotWalletFeatureModuleBinds
internal interface HotWalletFeatureModuleBinds {
@Binds
@Singleton
fun provideMnemonicRepository(repository: DefaultMnemonicRepository): MnemonicRepository
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.hotwallet.manualbackup.check
import androidx.compose.runtime.Composable
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.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.features.hotwallet.manualbackup.check.model.ManualBackupCheckModel
import com.tangem.features.hotwallet.manualbackup.check.ui.ManualBackupCheckContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class ManualBackupCheckComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: ManualBackupCheckModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
ManualBackupCheckContent(
state = state,
modifier = modifier,
)
}
interface ModelCallbacks {
fun onCompleteClick()
}
data class Params(
val generatedWords: Mnemonic,
val callbacks: ModelCallbacks,
)
}

View file

@ -0,0 +1,18 @@
package com.tangem.features.hotwallet.manualbackup.check.entity
import androidx.compose.ui.text.input.TextFieldValue
import kotlinx.collections.immutable.ImmutableList
internal data class ManualBackupCheckUM(
val onCompleteButtonClick: () -> Unit,
val wordFields: ImmutableList<WordField>,
val completeButtonEnabled: Boolean,
val completeButtonProgress: Boolean,
) {
data class WordField(
val index: Int,
val word: TextFieldValue,
val error: Boolean,
val onChange: (TextFieldValue) -> Unit,
)
}

View file

@ -0,0 +1,105 @@
package com.tangem.features.hotwallet.manualbackup.check.model
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.hotwallet.manualbackup.check.ManualBackupCheckComponent
import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
import kotlin.Boolean
import kotlin.Int
import kotlin.String
import kotlin.Suppress
import kotlin.collections.List
import kotlin.collections.all
import kotlin.collections.map
import kotlin.error
@Stable
@ModelScoped
internal class ManualBackupCheckModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<ManualBackupCheckComponent.Params>()
private val callbacks = params.callbacks
internal val uiState: StateFlow<ManualBackupCheckUM>
field = MutableStateFlow(getInitialUIState())
@Suppress("MagicNumber")
private fun getInitialUIState(): ManualBackupCheckUM {
val wordFields = List(WORD_FIELD_INDICES.size) { index ->
val shownIndex = WORD_FIELD_INDICES[index]
ManualBackupCheckUM.WordField(
index = shownIndex,
word = TextFieldValue(""),
error = false,
onChange = { textFieldValue ->
updateWordField(shownIndex, textFieldValue)
},
)
}.toImmutableList()
return ManualBackupCheckUM(
wordFields = wordFields,
completeButtonEnabled = false,
completeButtonProgress = false,
onCompleteButtonClick = {
val currentUIState = uiState.value
if (currentUIState.completeButtonEnabled) {
callbacks.onCompleteClick()
}
},
)
}
private fun updateWordField(shownIndex: Int, newText: TextFieldValue) {
uiState.update { currentState ->
val updatedFields = currentState.wordFields.map { wordField ->
if (wordField.index == shownIndex) {
val isCorrect = checkWordField(newText.text, shownIndex)
wordField.copy(
word = newText,
error = !isCorrect,
)
} else {
wordField
}
}.toImmutableList()
val allFieldsCorrect = updatedFields.all { field ->
checkWordField(field.word.text, field.index)
}
currentState.copy(
wordFields = updatedFields,
completeButtonEnabled = allFieldsCorrect,
)
}
}
private fun checkWordField(word: String, shownIndex: Int): Boolean {
val generatedWords = params.generatedWords
val wordList = generatedWords.mnemonicComponents
return if (shownIndex <= wordList.size) {
wordList[shownIndex - 1] == word
} else {
false
}
}
companion object {
private val WORD_FIELD_INDICES = listOf(2, 7, 11)
}
}

View file

@ -0,0 +1,147 @@
package com.tangem.features.hotwallet.manualbackup.check.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.components.OutlineTextField
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.manualbackup.check.entity.ManualBackupCheckUM
import kotlinx.collections.immutable.persistentListOf
@Composable
internal fun ManualBackupCheckContent(state: ManualBackupCheckUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.imePadding(),
) {
Column(
Modifier
.verticalScroll(rememberScrollState())
.weight(1f),
) {
Text(
text = stringResourceSafe(R.string.onboarding_seed_user_validation_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(top = 48.dp, start = 36.dp, end = 36.dp, bottom = 16.dp)
.fillMaxWidth(),
)
Text(
text = stringResourceSafe(R.string.onboarding_seed_user_validation_message),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier
.padding(horizontal = 36.dp)
.fillMaxWidth(),
)
Fields(
state = state,
modifier = Modifier
.padding(vertical = 30.dp, horizontal = 16.dp),
)
}
PrimaryButton(
modifier = Modifier
.padding(16.dp)
.imePadding()
.fillMaxWidth(),
text = stringResourceSafe(id = R.string.common_continue),
enabled = state.completeButtonEnabled,
showProgress = state.completeButtonProgress,
onClick = state.onCompleteButtonClick,
)
}
}
@Composable
private fun Fields(state: ManualBackupCheckUM, modifier: Modifier = Modifier) {
val focusManager = LocalFocusManager.current
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
DisableContextMenu {
state.wordFields.fastForEachIndexed { index, field ->
OutlineTextField(
value = field.word,
onValueChange = field.onChange,
label = field.index.toString(),
isError = field.error,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = if (index == state.wordFields.lastIndex) ImeAction.Done else ImeAction.Next,
),
keyboardActions = KeyboardActions(
onDone = { focusManager.clearFocus() },
onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) },
),
)
}
}
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
ManualBackupCheckContent(
state = ManualBackupCheckUM(
onCompleteButtonClick = {},
wordFields = persistentListOf(
ManualBackupCheckUM.WordField(
index = 2,
word = TextFieldValue(text = "word"),
onChange = {},
error = false,
),
ManualBackupCheckUM.WordField(
index = 7,
word = TextFieldValue(text = "wor"),
onChange = {},
error = true,
),
ManualBackupCheckUM.WordField(
index = 11,
word = TextFieldValue(text = "word"),
onChange = {},
error = false,
),
),
completeButtonEnabled = false,
completeButtonProgress = false,
),
)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.hotwallet.manualbackup.completed
import androidx.compose.runtime.Composable
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.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.manualbackup.completed.ui.ManualBackupCompletedContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class ManualBackupCompletedComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: ManualBackupCompletedModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
ManualBackupCompletedContent(
state = state,
modifier = modifier,
)
}
interface ModelCallbacks {
fun onContinueClick()
}
data class Params(
val callbacks: ModelCallbacks,
)
}

View file

@ -1,26 +1,26 @@
package com.tangem.features.hotwallet.addexistingwallet.im.port
package com.tangem.features.hotwallet.manualbackup.completed
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
import com.tangem.features.hotwallet.manualbackup.completed.entity.ManualBackupCompletedUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ModelScoped
internal class AddExistingWalletImportModel @Inject constructor(
internal class ManualBackupCompletedModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params: AddExistingWalletImportComponent.Params = paramsContainer.require()
private val params: ManualBackupCompletedComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<AddExistingWalletImportUM>
internal val uiState: StateFlow<ManualBackupCompletedUM>
field = MutableStateFlow(
AddExistingWalletImportUM(
onBackClick = params.callbacks::onBackClick,
ManualBackupCompletedUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.hotwallet.manualbackup.completed.entity
internal data class ManualBackupCompletedUM(
val onContinueClick: () -> Unit,
)

View file

@ -0,0 +1,86 @@
package com.tangem.features.hotwallet.manualbackup.completed.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.features.hotwallet.manualbackup.completed.entity.ManualBackupCompletedUM
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ManualBackupCompletedContent(state: ManualBackupCompletedUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(R.drawable.ic_success_blue_76),
contentDescription = null,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 48.dp,
top = 20.dp,
end = 48.dp,
),
text = stringResourceSafe(R.string.backup_complete_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 48.dp,
top = 12.dp,
end = 48.dp,
),
text = stringResourceSafe(R.string.backup_complete_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.weight(2f))
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.common_continue),
showProgress = false,
enabled = true,
onClick = state.onContinueClick,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewManualBackupCompletedContent() {
TangemThemePreview {
ManualBackupCompletedContent(
state = ManualBackupCompletedUM(
onContinueClick = {},
),
)
}
}

View file

@ -0,0 +1,37 @@
package com.tangem.features.hotwallet.manualbackup.phrase
import androidx.compose.runtime.Composable
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.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.manualbackup.phrase.model.ManualBackupPhraseModel
import com.tangem.features.hotwallet.manualbackup.phrase.ui.ManualBackupPhraseContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class ManualBackupPhraseComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: ManualBackupPhraseModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
ManualBackupPhraseContent(
state = state,
modifier = modifier,
)
}
interface ModelCallbacks {
fun onContinueClick()
}
data class Params(
val callbacks: ModelCallbacks,
)
}

View file

@ -0,0 +1,14 @@
package com.tangem.features.hotwallet.manualbackup.phrase.entity
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
internal data class ManualBackupPhraseUM(
val onContinueClick: () -> Unit,
val words: ImmutableList<MnemonicGridItem> = persistentListOf(),
) {
data class MnemonicGridItem(
val index: Int,
val mnemonic: String,
)
}

View file

@ -0,0 +1,32 @@
package com.tangem.features.hotwallet.manualbackup.phrase.model
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.features.hotwallet.manualbackup.phrase.ManualBackupPhraseComponent
import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@Stable
@ModelScoped
internal class ManualBackupPhraseModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<ManualBackupPhraseComponent.Params>()
private val callbacks = params.callbacks
internal val uiState: StateFlow<ManualBackupPhraseUM>
field = MutableStateFlow(getInitialUIState())
private fun getInitialUIState(): ManualBackupPhraseUM {
return ManualBackupPhraseUM(
onContinueClick = callbacks::onContinueClick,
)
}
}

View file

@ -0,0 +1,181 @@
package com.tangem.features.hotwallet.manualbackup.phrase.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.impl.R
import com.tangem.features.hotwallet.manualbackup.phrase.entity.ManualBackupPhraseUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
internal fun ManualBackupPhraseContent(state: ManualBackupPhraseUM, modifier: Modifier = Modifier) {
Column(
modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize(),
) {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.imePadding()
.weight(1f),
) {
TitleBlock(
state = state,
modifier = Modifier.padding(top = 20.dp),
)
SeedPhraseGridBlock(
mnemonicGridItems = state.words,
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp, bottom = 32.dp),
)
}
Text(
text = stringResourceSafe(R.string.backup_seed_responsibility),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
)
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
text = stringResourceSafe(id = R.string.common_continue),
onClick = state.onContinueClick,
)
}
}
@Composable
private fun TitleBlock(state: ManualBackupPhraseUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.padding(horizontal = TangemTheme.dimens.size36)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Text(
text = stringResourceSafe(R.string.backup_seed_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResourceSafe(
R.string.backup_seed_description,
state.words.size,
),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth(),
)
}
}
@Composable
private fun SeedPhraseGridBlock(
mnemonicGridItems: ImmutableList<ManualBackupPhraseUM.MnemonicGridItem>,
modifier: Modifier = Modifier,
) {
VerticalGrid(
modifier = modifier,
items = mnemonicGridItems,
) { item ->
Row(
modifier = Modifier.padding(all = TangemTheme.dimens.size8),
verticalAlignment = Alignment.CenterVertically,
) {
if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
} else {
Text(
text = item.mnemonic,
style = TangemTheme.typography.button,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier.width(TangemTheme.dimens.size40),
text = "${item.index}.",
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
}
@Composable
private inline fun <T> VerticalGrid(
items: ImmutableList<T>,
modifier: Modifier = Modifier,
crossinline content: @Composable (T) -> Unit,
) {
val columnLength = items.size / 2
Row(
modifier = modifier,
horizontalArrangement = Arrangement.SpaceEvenly,
) {
repeat(2) { index ->
Column {
for (i in 0 until columnLength) {
val item = items[index * columnLength + i]
content(item)
}
}
}
}
}
@Preview(showBackground = true, widthDp = 360, heightDp = 640)
@Preview(showBackground = true, widthDp = 360, heightDp = 640, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
ManualBackupPhraseContent(
state = ManualBackupPhraseUM(
onContinueClick = {},
words = List(12) {
ManualBackupPhraseUM.MnemonicGridItem(
index = it + 1,
mnemonic = "word${it + 1}",
)
}.toImmutableList(),
),
)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.hotwallet.manualbackup.start
import androidx.compose.runtime.Composable
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.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.manualbackup.start.ui.ManualBackupStartContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class ManualBackupStartComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: ManualBackupStartModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
ManualBackupStartContent(
state = state,
modifier = modifier,
)
}
interface ModelCallbacks {
fun onContinueClick()
}
data class Params(
val callbacks: ModelCallbacks,
)
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.hotwallet.manualbackup.start
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStartUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ModelScoped
internal class ManualBackupStartModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params: ManualBackupStartComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<ManualBackupStartUM>
field = MutableStateFlow(
ManualBackupStartUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
}

View file

@ -0,0 +1,6 @@
package com.tangem.features.hotwallet.manualbackup.start.entity
internal data class ManualBackupStartUM(
val seepPhraseLength: Int = 12,
val onContinueClick: () -> Unit,
)

View file

@ -0,0 +1,138 @@
package com.tangem.features.hotwallet.manualbackup.start.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.manualbackup.start.entity.ManualBackupStartUM
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun ManualBackupStartContent(state: ManualBackupStartUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.padding(
start = 16.dp,
top = 24.dp,
end = 16.dp,
bottom = 16.dp,
),
) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 8.dp,
),
text = stringResourceSafe(R.string.backup_info_title),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
horizontal = 16.dp,
vertical = 8.dp,
),
text = stringResourceSafe(
R.string.backup_info_description,
state.seepPhraseLength.toString(),
),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
FeatureBlock(
modifier = Modifier
.padding(top = 24.dp),
title = stringResourceSafe(R.string.backup_info_save_title),
description = stringResourceSafe(
R.string.backup_info_save_description,
state.seepPhraseLength.toString(),
),
iconRes = R.drawable.ic_lock_24,
)
FeatureBlock(
modifier = Modifier
.padding(top = 24.dp),
title = stringResourceSafe(R.string.backup_info_keep_title),
description = stringResourceSafe(R.string.backup_info_keep_description),
iconRes = R.drawable.ic_settings_24,
)
Spacer(modifier = Modifier.weight(1f))
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
text = stringResourceSafe(R.string.common_continue),
showProgress = false,
enabled = true,
onClick = state.onContinueClick,
)
}
}
@Composable
private fun FeatureBlock(title: String, description: String, iconRes: Int, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
) {
Icon(
modifier = Modifier
.padding(horizontal = 12.dp),
painter = painterResource(iconRes),
contentDescription = null,
tint = TangemTheme.colors.icon.primary1,
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
) {
Text(
text = title,
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
modifier = Modifier
.padding(top = 4.dp),
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewManualBackupStartContent() {
TangemThemePreview {
ManualBackupStartContent(
state = ManualBackupStartUM(
onContinueClick = {},
),
)
}
}

View file

@ -0,0 +1,43 @@
package com.tangem.features.hotwallet.setaccesscode
import androidx.compose.runtime.Composable
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.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.core.ui.security.DisableScreenshotsDisposableEffect
import com.tangem.features.hotwallet.setaccesscode.ui.SetAccessCodeContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class SetAccessCodeComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: SetAccessCodeModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
SetAccessCodeContent(
state = state,
onBack = { model.onBack() },
modifier = modifier,
)
DisableScreenshotsDisposableEffect()
}
interface ModelCallbacks {
fun onBackClick()
fun onAccessCodeSet()
}
data class Params(
val callbacks: ModelCallbacks,
)
}

View file

@ -0,0 +1,84 @@
package com.tangem.features.hotwallet.setaccesscode
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.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import javax.inject.Inject
@Stable
@ModelScoped
internal class SetAccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params = paramsContainer.require<SetAccessCodeComponent.Params>()
internal val uiState: StateFlow<SetAccessCodeUM>
field = MutableStateFlow(getInitialState())
fun onBack() {
when (uiState.value.step) {
SetAccessCodeUM.Step.AccessCode -> {
params.callbacks.onBackClick()
}
SetAccessCodeUM.Step.ConfirmAccessCode -> {
uiState.update {
it.copy(
step = SetAccessCodeUM.Step.AccessCode,
accessCodeSecond = "",
)
}
}
}
}
private fun getInitialState() = SetAccessCodeUM(
step = SetAccessCodeUM.Step.AccessCode,
accessCodeFirst = "",
accessCodeSecond = "",
onAccessCodeFirstChange = ::onAccessCodeFirstChange,
onAccessCodeSecondChange = ::onAccessCodeSecondChange,
buttonEnabled = false,
onContinue = ::onContinue,
)
private fun onAccessCodeFirstChange(value: String) {
uiState.update {
it.copy(
accessCodeFirst = value,
buttonEnabled = value.length == uiState.value.accessCodeLength,
)
}
}
private fun onAccessCodeSecondChange(value: String) {
uiState.update {
it.copy(
accessCodeSecond = value,
buttonEnabled = uiState.value.accessCodeFirst == uiState.value.accessCodeSecond,
)
}
}
private fun onContinue() {
when (uiState.value.step) {
SetAccessCodeUM.Step.AccessCode -> {
uiState.update {
it.copy(
step = SetAccessCodeUM.Step.ConfirmAccessCode,
)
}
}
SetAccessCodeUM.Step.ConfirmAccessCode -> {
params.callbacks.onAccessCodeSet()
}
}
}
}

View file

@ -0,0 +1,22 @@
package com.tangem.features.hotwallet.setaccesscode.entity
internal data class SetAccessCodeUM(
val step: Step,
val accessCodeFirst: String,
val accessCodeSecond: String,
val onAccessCodeFirstChange: (String) -> Unit,
val onAccessCodeSecondChange: (String) -> Unit,
val buttonEnabled: Boolean,
val onContinue: () -> Unit,
) {
val accessCodeLength: Int = ACCESS_CODE_LENGTH
enum class Step {
AccessCode,
ConfirmAccessCode,
}
companion object {
private const val ACCESS_CODE_LENGTH = 6
}
}

View file

@ -0,0 +1,59 @@
package com.tangem.features.hotwallet.setaccesscode.ui
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemAnimations
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM.Step.*
import com.tangem.core.res.R
@Composable
internal fun SetAccessCodeContent(state: SetAccessCodeUM, onBack: () -> Unit, modifier: Modifier = Modifier) {
BackHandler(onBack = onBack)
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
AnimatedContent(
modifier = Modifier.weight(1f),
targetState = state.step,
transitionSpec = TangemAnimations.AnimatedContent
.slide { initial, target -> target.ordinal > initial.ordinal },
label = "AnimatedContent",
) { step ->
when (step) {
AccessCode -> SetAccessCodeEnter(
modifier = Modifier.padding(top = 16.dp),
state = state,
reEnterAccessCodeState = false,
)
ConfirmAccessCode -> SetAccessCodeEnter(
modifier = Modifier.padding(top = 16.dp),
state = state,
reEnterAccessCodeState = true,
)
}
}
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.imePadding(),
text = if (state.step == ConfirmAccessCode) {
stringResourceSafe(R.string.common_confirm)
} else {
stringResourceSafe(R.string.common_continue)
},
onClick = state.onContinue,
)
}
}

View file

@ -0,0 +1,125 @@
package com.tangem.features.hotwallet.setaccesscode.ui
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.res.R
import com.tangem.core.ui.components.fields.PinTextField
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.setaccesscode.entity.SetAccessCodeUM
@Composable
internal fun SetAccessCodeEnter(
state: SetAccessCodeUM,
reEnterAccessCodeState: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary)
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier
.padding(top = 56.dp)
.align(Alignment.CenterHorizontally),
text = if (reEnterAccessCodeState) {
stringResourceSafe(R.string.access_code_confirm_title)
} else {
stringResourceSafe(R.string.access_code_create_title)
},
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier
.padding(16.dp)
.align(Alignment.CenterHorizontally),
text = if (reEnterAccessCodeState) {
stringResourceSafe(R.string.access_code_confirm_description)
} else {
stringResourceSafe(
R.string.access_code_create_description,
state.accessCodeLength,
)
},
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
Box(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp),
contentAlignment = Alignment.Center,
) {
PinTextField(
length = state.accessCodeLength,
isPasswordVisual = true,
value = if (reEnterAccessCodeState) {
state.accessCodeSecond
} else {
state.accessCodeFirst
},
onValueChange = if (reEnterAccessCodeState) {
state.onAccessCodeSecondChange
} else {
state.onAccessCodeFirstChange
},
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
SetAccessCodeEnter(
reEnterAccessCodeState = false,
state = SetAccessCodeUM(
step = SetAccessCodeUM.Step.AccessCode,
accessCodeFirst = "",
accessCodeSecond = "",
onAccessCodeFirstChange = {},
onAccessCodeSecondChange = {},
buttonEnabled = false,
onContinue = {},
),
)
}
}
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Preview2() {
TangemThemePreview {
SetAccessCodeEnter(
reEnterAccessCodeState = true,
state = SetAccessCodeUM(
step = SetAccessCodeUM.Step.ConfirmAccessCode,
accessCodeFirst = "",
accessCodeSecond = "",
onAccessCodeFirstChange = {},
onAccessCodeSecondChange = {},
buttonEnabled = false,
onContinue = {},
),
)
}
}

View file

@ -0,0 +1,36 @@
package com.tangem.features.hotwallet.setupfinished
import androidx.compose.runtime.Composable
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.model.getOrCreateModel
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.features.hotwallet.setupfinished.ui.MobileWalletSetupFinishedContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
internal class MobileWalletSetupFinishedComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: Params,
) : ComposableContentComponent, AppComponentContext by context {
private val model: MobileWalletSetupFinishedModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
MobileWalletSetupFinishedContent(
state = state,
modifier = modifier,
)
}
interface ModelCallbacks {
fun onContinueClick()
}
data class Params(
val callbacks: ModelCallbacks,
)
}

View file

@ -0,0 +1,26 @@
package com.tangem.features.hotwallet.setupfinished
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.features.hotwallet.setupfinished.entity.MobileWalletSetupFinishedUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
@ModelScoped
internal class MobileWalletSetupFinishedModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params: MobileWalletSetupFinishedComponent.Params = paramsContainer.require()
internal val uiState: StateFlow<MobileWalletSetupFinishedUM>
field = MutableStateFlow(
MobileWalletSetupFinishedUM(
onContinueClick = params.callbacks::onContinueClick,
),
)
}

View file

@ -0,0 +1,5 @@
package com.tangem.features.hotwallet.setupfinished.entity
internal data class MobileWalletSetupFinishedUM(
val onContinueClick: () -> Unit,
)

View file

@ -0,0 +1,119 @@
package com.tangem.features.hotwallet.setupfinished.ui
import android.content.res.Configuration
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.airbnb.lottie.compose.LottieAnimation
import com.airbnb.lottie.compose.LottieCompositionSpec
import com.airbnb.lottie.compose.animateLottieCompositionAsState
import com.airbnb.lottie.compose.rememberLottieComposition
import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.FullScreen
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.features.hotwallet.setupfinished.entity.MobileWalletSetupFinishedUM
@Suppress("LongMethod")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun MobileWalletSetupFinishedContent(state: MobileWalletSetupFinishedUM, modifier: Modifier = Modifier) {
val composition by rememberLottieComposition(spec = LottieCompositionSpec.RawRes(R.raw.anim_confetti))
val progress by animateLottieCompositionAsState(composition)
var showConfetti by remember { mutableStateOf(false) }
Column(
modifier = modifier
.background(TangemTheme.colors.background.primary)
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(modifier = Modifier.weight(1f))
Image(
painter = painterResource(R.drawable.ic_success_blue_76),
contentDescription = null,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 48.dp,
top = 20.dp,
end = 48.dp,
),
text = stringResourceSafe(R.string.onboarding_done_header),
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
textAlign = TextAlign.Center,
)
Text(
modifier = Modifier
.fillMaxWidth()
.padding(
start = 48.dp,
top = 12.dp,
end = 48.dp,
),
text = stringResourceSafe(R.string.backup_complete_description),
style = TangemTheme.typography.body1,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.weight(2f))
PrimaryButton(
modifier = Modifier.fillMaxWidth(),
text = stringResourceSafe(R.string.common_continue),
showProgress = false,
enabled = true,
onClick = state.onContinueClick,
)
}
if (showConfetti) {
FullScreen(notTouchable = true) {
LottieAnimation(
composition = composition,
progress = { progress },
)
}
}
LaunchedEffect(Unit) {
showConfetti = true
}
LaunchedEffect(progress == 1f) {
if (progress == 1f) {
showConfetti = false
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewMobileWalletSetupFinishedContent() {
TangemThemePreview {
MobileWalletSetupFinishedContent(
state = MobileWalletSetupFinishedUM(
onContinueClick = {},
),
)
}
}

View file

@ -0,0 +1,39 @@
package com.tangem.features.hotwallet.walletbackup.component
import androidx.compose.runtime.Composable
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.model.getOrCreateModel
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.walletbackup.model.WalletBackupModel
import com.tangem.features.hotwallet.walletbackup.ui.WalletBackupContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
internal class DefaultWalletBackupComponent @AssistedInject constructor(
@Assisted private val context: AppComponentContext,
@Assisted private val params: WalletBackupComponent.Params,
) : WalletBackupComponent, AppComponentContext by context {
private val model: WalletBackupModel = getOrCreateModel(params)
@Composable
override fun Content(modifier: Modifier) {
val state by model.uiState.collectAsStateWithLifecycle()
WalletBackupContent(
state = state,
modifier = modifier,
)
}
@AssistedFactory
interface Factory : WalletBackupComponent.Factory {
override fun create(
context: AppComponentContext,
params: WalletBackupComponent.Params,
): DefaultWalletBackupComponent
}
}

View file

@ -0,0 +1,27 @@
package com.tangem.features.hotwallet.walletbackup.di
import com.tangem.core.decompose.model.Model
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.walletbackup.component.DefaultWalletBackupComponent
import com.tangem.features.hotwallet.walletbackup.model.WalletBackupModel
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.ClassKey
import dagger.multibindings.IntoMap
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface WalletBackupModule {
@Binds
@IntoMap
@ClassKey(WalletBackupModel::class)
fun bindWalletBackupModel(model: WalletBackupModel): Model
@Binds
@Singleton
fun bindWalletBackupComponentFactory(factory: DefaultWalletBackupComponent.Factory): WalletBackupComponent.Factory
}

View file

@ -0,0 +1,15 @@
package com.tangem.features.hotwallet.walletbackup.entity
internal data class WalletBackupUM(
val onBackClick: () -> Unit,
val recoveryPhraseStatus: BackupStatus,
val googleDriveStatus: BackupStatus,
val onRecoveryPhraseClick: () -> Unit,
val onGoogleDriveClick: () -> Unit,
)
internal sealed class BackupStatus {
object Done : BackupStatus()
object ComingSoon : BackupStatus()
object NoBackup : BackupStatus()
}

View file

@ -0,0 +1,54 @@
package com.tangem.features.hotwallet.walletbackup.model
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.wallets.usecase.GetUserWalletUseCase
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus
import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import javax.inject.Inject
@ModelScoped
internal class WalletBackupModel @Inject constructor(
paramsContainer: ParamsContainer,
getWalletUseCase: GetUserWalletUseCase,
private val router: Router,
override val dispatchers: CoroutineDispatcherProvider,
) : Model() {
private val params: WalletBackupComponent.Params = paramsContainer.require()
val uiState: StateFlow<WalletBackupUM>
field = MutableStateFlow(
WalletBackupUM(
onBackClick = { router.pop() },
recoveryPhraseStatus = BackupStatus.NoBackup,
googleDriveStatus = BackupStatus.ComingSoon,
onRecoveryPhraseClick = { },
onGoogleDriveClick = { },
),
)
init {
getWalletUseCase.invokeFlow(params.userWalletId)
.map { it.getOrNull() }
.distinctUntilChanged()
.onEach {
updateBackupStatuses()
}
.launchIn(modelScope)
}
private fun updateBackupStatuses() {
uiState.update { currentState ->
currentState.copy(
recoveryPhraseStatus = BackupStatus.NoBackup,
googleDriveStatus = BackupStatus.ComingSoon,
)
}
}
}

View file

@ -0,0 +1,155 @@
package com.tangem.features.hotwallet.walletbackup.ui
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
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 androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.appbar.AppBarWithBackButton
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.hotwallet.walletbackup.entity.BackupStatus
import com.tangem.features.hotwallet.walletbackup.entity.WalletBackupUM
import com.tangem.features.hotwallet.common.ui.OptionBlock
import com.tangem.core.ui.R
import com.tangem.features.hotwallet.common.ui.DISABLED_COLORS_ALPHA
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun WalletBackupContent(state: WalletBackupUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.background(TangemTheme.colors.background.secondary)
.fillMaxSize()
.systemBarsPadding(),
) {
AppBarWithBackButton(
text = stringResourceSafe(R.string.common_backup),
onBackClick = state.onBackClick,
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
) {
OptionBlock(
modifier = Modifier.fillMaxWidth(),
title = stringResourceSafe(R.string.hw_backup_seed_title),
description = stringResourceSafe(R.string.hw_backup_seed_description),
badge = {
BackupStatusBadge(status = state.recoveryPhraseStatus)
},
onClick = state.onRecoveryPhraseClick,
enabled = true,
backgroundColor = TangemTheme.colors.background.primary,
)
OptionBlock(
modifier = Modifier.fillMaxWidth(),
title = stringResourceSafe(R.string.hw_backup_google_drive_title),
description = stringResourceSafe(R.string.hw_backup_google_drive_description),
badge = {
BackupStatusBadge(status = state.googleDriveStatus)
},
onClick = state.onGoogleDriveClick,
enabled = state.googleDriveStatus != BackupStatus.ComingSoon,
backgroundColor = TangemTheme.colors.background.primary,
)
}
}
}
@Composable
private fun BackupStatusBadge(status: BackupStatus, modifier: Modifier = Modifier) {
val text = when (status) {
BackupStatus.Done -> stringResourceSafe(R.string.common_done)
BackupStatus.ComingSoon -> stringResourceSafe(R.string.common_coming_soon)
BackupStatus.NoBackup -> stringResourceSafe(R.string.hw_backup_no_backup)
}
val backgroundColor by animateColorAsState(
targetValue = when (status) {
BackupStatus.Done -> TangemTheme.colors.text.accent.copy(alpha = 0.1f)
BackupStatus.ComingSoon -> TangemTheme.colors.control.unchecked.copy(DISABLED_COLORS_ALPHA)
BackupStatus.NoBackup -> TangemTheme.colors.text.warning.copy(alpha = 0.1f)
},
)
val textColor by animateColorAsState(
targetValue = when (status) {
BackupStatus.Done -> TangemTheme.colors.text.accent
BackupStatus.ComingSoon -> TangemTheme.colors.text.secondary.copy(DISABLED_COLORS_ALPHA)
BackupStatus.NoBackup -> TangemTheme.colors.text.warning
},
)
AnimatedContent(targetState = text) { text ->
Box(
modifier = modifier
.padding(horizontal = 4.dp)
.background(
color = backgroundColor,
shape = TangemTheme.shapes.roundedCorners8,
)
.padding(horizontal = 8.dp, vertical = 4.dp),
) {
Text(
text = text,
style = TangemTheme.typography.caption1,
color = textColor,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun WalletBackupContentPreview(@PreviewParameter(WalletBackupUMProvider::class) state: WalletBackupUM) {
TangemThemePreview {
WalletBackupContent(state)
}
}
private class WalletBackupUMProvider : CollectionPreviewParameterProvider<WalletBackupUM>(
collection = listOf(
WalletBackupUM(
recoveryPhraseStatus = BackupStatus.NoBackup,
googleDriveStatus = BackupStatus.ComingSoon,
onBackClick = {},
onRecoveryPhraseClick = {},
onGoogleDriveClick = {},
),
WalletBackupUM(
recoveryPhraseStatus = BackupStatus.NoBackup,
googleDriveStatus = BackupStatus.NoBackup,
onBackClick = {},
onRecoveryPhraseClick = {},
onGoogleDriveClick = {},
),
WalletBackupUM(
recoveryPhraseStatus = BackupStatus.Done,
googleDriveStatus = BackupStatus.Done,
onBackClick = {},
onRecoveryPhraseClick = {},
onGoogleDriveClick = {},
),
),
)

View file

@ -11,7 +11,7 @@ import com.sumsub.sns.core.theme.colors
import com.sumsub.sns.core.theme.metrics
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.pay.repository.KycRepository
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableBottomSheetComponent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
interface AddCustomTokenComponent : ComposableBottomSheetComponent {

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
interface ChooseManagedTokensComponent : ComposableContentComponent {

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
interface ManageTokensComponent : ComposableContentComponent {

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
interface OnboardingManageTokensComponent : ComposableContentComponent {

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.choosetoken.entity
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableDialogComponent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
internal interface CustomTokenDerivationInputComponent : ComposableDialogComponent {

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.entity.customtoken.CustomTokenFormValues
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork

View file

@ -2,7 +2,7 @@ package com.tangem.features.managetokens.component
import com.tangem.core.decompose.factory.ComponentFactory
import com.tangem.core.ui.decompose.ComposableContentComponent
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.entity.customtoken.SelectedDerivationPath
import com.tangem.features.managetokens.entity.customtoken.SelectedNetwork

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig

View file

@ -4,7 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM

View file

@ -1,7 +1,7 @@
package com.tangem.features.managetokens.entity.customtoken
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable

View file

@ -1,6 +1,6 @@
package com.tangem.features.managetokens.entity.customtoken
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable

View file

@ -1,6 +1,6 @@
package com.tangem.features.managetokens.entity.managetokens
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
@Serializable

View file

@ -11,7 +11,7 @@ import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.GetSupportedNetworksUseCase
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.DerivationPathSelector
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent.Params.NetworkSelector

View file

@ -19,7 +19,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.card.HasMissedDerivationsUseCase
import com.tangem.domain.managetokens.SaveManagedTokensUseCase
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.analytics.CustomTokenAnalyticsEvent
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensComponent

View file

@ -17,7 +17,7 @@ import com.tangem.core.ui.components.bottomsheets.sheet.TangemBottomSheet
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.AddCustomTokenComponent
import com.tangem.features.managetokens.component.preview.PreviewAddCustomTokenComponent
import com.tangem.features.managetokens.entity.customtoken.AddCustomTokenConfig

View file

@ -31,7 +31,7 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.CustomTokenSelectorComponent
import com.tangem.features.managetokens.component.preview.PreviewCustomTokenSelectorComponent
import com.tangem.features.managetokens.entity.customtoken.CustomTokenSelectorUM

View file

@ -55,7 +55,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.utils.WindowInsetsZero
import com.tangem.core.ui.utils.rememberHideKeyboardNestedScrollConnection
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.component.preview.PreviewManageTokensComponent

View file

@ -11,7 +11,7 @@ import com.tangem.domain.managetokens.model.exceptoin.CustomTokenFormValidationE
import com.tangem.domain.managetokens.model.exceptoin.FindTokenException
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveInAndJoin
import kotlinx.coroutines.coroutineScope

View file

@ -14,7 +14,7 @@ import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.managetokens.*
import com.tangem.domain.managetokens.model.*
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.analytics.ManageTokensAnalyticEvent
import com.tangem.features.managetokens.component.ManageTokensSource
import com.tangem.features.managetokens.entity.item.CurrencyItemUM

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.utils.list
import com.tangem.domain.managetokens.model.ManageTokensListConfig
import com.tangem.domain.managetokens.model.ManageTokensUpdateAction
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.managetokens.entity.item.CurrencyItemUM
import com.tangem.pagination.Batch
import com.tangem.pagination.BatchAction

View file

@ -3,7 +3,7 @@ package com.tangem.features.managetokens.utils.list
import com.tangem.domain.managetokens.model.CurrencyUnsupportedState
import com.tangem.domain.managetokens.model.ManagedCryptoCurrency
import com.tangem.domain.models.network.Network
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWalletId
internal interface ManageTokensUiActions {

View file

@ -15,6 +15,7 @@ dependencies {
/* Project - API */
api(projects.features.markets.api)
api(projects.features.onramp.api)
api(projects.features.sendV2.api)
/* Data */
implementation(projects.data.common)
@ -74,7 +75,6 @@ dependencies {
implementation(projects.core.analytics)
implementation(projects.core.analytics.models)
implementation(projects.core.navigation)
implementation(projects.core.deepLinks)
/* Common */
implementation(projects.common.ui)

View file

@ -3,7 +3,7 @@ package com.tangem.features.markets.deeplink
import arrow.core.getOrElse
import com.tangem.common.routing.AppRoute
import com.tangem.common.routing.AppRouter
import com.tangem.core.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.common.routing.deeplink.DeeplinkConst.TOKEN_ID_KEY
import com.tangem.data.common.currency.getTokenIconUrlFromDefaultHost
import com.tangem.domain.appcurrency.GetSelectedAppCurrencyUseCase
import com.tangem.domain.appcurrency.model.AppCurrency

View file

@ -6,8 +6,8 @@ import com.tangem.domain.tokens.error.TokenListError
import com.tangem.domain.tokens.model.CryptoCurrencyStatus
import com.tangem.domain.tokens.model.TokenActionsState
import com.tangem.domain.tokens.model.TotalFiatBalance
import com.tangem.domain.wallets.models.UserWallet
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
/**
* Portfolio data. Combined data from all flows that required to setup portfolio

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