Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-18 12:34:18 +03:00
parent 48b8c77f5d
commit cb795ebf3d
27 changed files with 432 additions and 262 deletions

@ -1 +1 @@
Subproject commit 23aae9e3496d89a021ac9a0833b54b49635bb193
Subproject commit 3ac868e93f88498258867d457f8b8c4577b40f98

View file

@ -77,7 +77,9 @@ internal class DefaultUserWalletsListRepository(
override suspend fun userWalletsSync(): List<UserWallet> {
load()
return userWallets.value!!
return requireNotNull(userWallets.value) {
"This should never happen"
}
}
override suspend fun selectedUserWalletSync(): UserWallet? {

View file

@ -19,6 +19,7 @@ import com.tangem.features.hotwallet.CreateMobileWalletComponent
import com.tangem.features.hotwallet.WalletActivationComponent
import com.tangem.features.hotwallet.WalletBackupComponent
import com.tangem.features.hotwallet.UpdateAccessCodeComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.managetokens.component.ChooseManagedTokensComponent
import com.tangem.features.managetokens.component.ManageTokensComponent
import com.tangem.features.managetokens.component.ManageTokensSource
@ -52,6 +53,7 @@ import com.tangem.tap.routing.component.RoutingComponent.Child
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Inject
import com.tangem.features.walletconnect.components.WalletConnectEntryComponent as RedesignedWalletConnectComponent
import com.tangem.features.welcome.WelcomeComponent as NewWelcomeComponent
@ActivityScoped
@Suppress("LongParameterList", "LargeClass")
@ -70,6 +72,7 @@ internal class ChildFactory @Inject constructor(
private val swapSelectTokensComponentFactory: SwapSelectTokensComponent.Factory,
private val onboardingEntryComponentFactory: OnboardingEntryComponent.Factory,
private val welcomeComponentFactory: WelcomeComponent.Factory,
private val newWelcomeComponentFactory: NewWelcomeComponent.Factory,
private val storiesComponentFactory: StoriesComponent.Factory,
private val stakingComponentFactory: StakingComponent.Factory,
private val swapComponentFactory: SwapComponent.Factory,
@ -102,6 +105,7 @@ internal class ChildFactory @Inject constructor(
private val sendWithSwapComponentFactory: SendWithSwapComponent.Factory,
private val sendEntryPointComponentFactory: SendEntryPointComponent.Factory,
private val walletConnectFeatureToggles: WalletConnectFeatureToggles,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
) {
@Suppress("LongMethod", "CyclomaticComplexMethod")
@ -138,14 +142,25 @@ internal class ChildFactory @Inject constructor(
)
}
is AppRoute.Welcome -> {
createComponentChild(
context = context,
params = WelcomeComponent.Params(
launchMode = route.launchMode,
intent = route.intent,
),
componentFactory = welcomeComponentFactory,
)
if (hotWalletFeatureToggles.isHotWalletEnabled) {
createComponentChild(
context = context,
params = NewWelcomeComponent.Params(
launchMode = route.launchMode,
intent = route.intent,
),
componentFactory = newWelcomeComponentFactory,
)
} else {
createComponentChild(
context = context,
params = WelcomeComponent.Params(
launchMode = route.launchMode,
intent = route.intent,
),
componentFactory = welcomeComponentFactory,
)
}
}
is AppRoute.WalletSettings -> {
createComponentChild(

View file

@ -3,7 +3,9 @@ package com.tangem.common.ui.userwallet
import android.content.res.Configuration
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -22,6 +24,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.dp
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import com.tangem.common.ui.R
@ -184,6 +187,19 @@ fun CardImage(imageState: UserWalletItemUM.ImageState, modifier: Modifier = Modi
radius = TangemTheme.dimens.size2,
)
}
is UserWalletItemUM.ImageState.MobileWallet -> {
Image(
modifier = Modifier
.size(36.dp)
.background(
color = TangemTheme.colors.field.focused,
shape = RoundedCornerShape(10.dp),
)
.padding(6.dp),
imageVector = ImageVector.vectorResource(R.drawable.ic_mobile_wallet_icon_24),
contentDescription = null,
)
}
is UserWalletItemUM.ImageState.Image -> {
val verifiedArtwork = imageState.artwork.verifiedArtwork
if (verifiedArtwork != null) {
@ -364,6 +380,18 @@ private class UserWalletItemUMPreviewProvider : PreviewParameterProvider<UserWal
isEnabled = true,
onClick = {},
),
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Failed,
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = false,
),
imageState = UserWalletItemUM.ImageState.MobileWallet,
isEnabled = true,
onClick = {},
),
)
private fun getInformation(cardCount: Int): UserWalletItemUM.Information.Loaded {

View file

@ -2,8 +2,8 @@ package com.tangem.common.ui.userwallet.converter
import com.tangem.common.ui.R
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.components.label.entity.LabelUM
import com.tangem.core.ui.components.label.entity.LabelStyle
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
@ -35,6 +35,7 @@ class UserWalletItemUMConverter(
private val appCurrency: AppCurrency? = null,
private val balance: TotalFiatBalance? = null,
private val isBalanceHidden: Boolean = false,
private val authMode: Boolean = false,
private val endIcon: UserWalletItemUM.EndIcon = UserWalletItemUM.EndIcon.None,
private val artwork: ArtworkModel? = null,
) : Converter<UserWallet, UserWalletItemUM> {
@ -48,24 +49,38 @@ class UserWalletItemUMConverter(
name = stringReference(name),
information = getInfo(userWallet = this),
balance = getBalanceInfo(userWallet = this),
isEnabled = !isLocked,
isEnabled = isEnabled(userWallet = this),
endIcon = endIcon,
onClick = { onClick(value.walletId) },
imageState = artwork?.let {
UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(it))
} ?: UserWalletItemUM.ImageState.Loading,
label = if (this is UserWallet.Hot && !this.backedUp) {
LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
)
} else {
null
},
imageState = getImageState(userWallet = value),
label = getLabelOrNull(userWallet = this),
)
}
}
private fun isEnabled(userWallet: UserWallet): Boolean {
return authMode || userWallet.isLocked.not()
}
private fun getLabelOrNull(userWallet: UserWallet): LabelUM? {
return if (authMode.not() && userWallet is UserWallet.Hot && !userWallet.backedUp) {
LabelUM(
text = resourceReference(R.string.hw_backup_no_backup),
style = LabelStyle.WARNING,
)
} else {
null
}
}
private fun getImageState(userWallet: UserWallet): UserWalletItemUM.ImageState {
return when {
userWallet is UserWallet.Hot -> UserWalletItemUM.ImageState.MobileWallet
artwork != null -> UserWalletItemUM.ImageState.Image(artworkUMConverter.convert(artwork))
else -> UserWalletItemUM.ImageState.Loading
}
}
private fun getInfo(userWallet: UserWallet): UserWalletItemUM.Information.Loaded {
val text = when (userWallet) {
is UserWallet.Cold -> {

View file

@ -56,6 +56,8 @@ data class UserWalletItemUM(
data object Loading : ImageState()
data object MobileWallet : ImageState()
data class Image(
val artwork: ArtworkUM,
) : ImageState()

View file

@ -0,0 +1,19 @@
<vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
<path android:pathData="M6.839,11.095C7.657,11.095 8.32,11.759 8.32,12.576V20.148C8.32,20.966 7.657,21.629 6.839,21.629H2.864C2.046,21.629 1.383,20.966 1.383,20.148V12.576C1.383,11.759 2.046,11.095 2.864,11.095H6.839ZM14.064,2.37C14.831,2.37 15.401,2.37 15.894,2.502C17.227,2.858 18.268,3.897 18.625,5.226C18.709,5.54 18.74,5.885 18.751,6.291C18.895,6.304 19.034,6.319 19.167,6.337C20.062,6.457 20.853,6.719 21.486,7.351C22.119,7.983 22.382,8.772 22.503,9.665C22.618,10.516 22.617,11.591 22.617,12.895V14.957C22.617,16.261 22.618,17.336 22.503,18.187C22.382,19.08 22.12,19.869 21.486,20.501C20.853,21.133 20.062,21.395 19.167,21.515C18.314,21.629 17.236,21.629 15.929,21.629H9.999C9.987,21.629 9.975,21.628 9.963,21.628C10.176,21.18 10.297,20.678 10.297,20.149V12.577C10.297,10.668 8.749,9.12 6.84,9.12H2.865C2.335,9.12 1.832,9.24 1.383,9.453V5.259C1.383,3.664 2.679,2.37 4.278,2.37H14.064ZM18.406,12.436C17.468,12.436 16.707,13.196 16.707,14.134C16.707,15.073 17.468,15.833 18.406,15.833C19.345,15.833 20.105,15.073 20.105,14.134C20.105,13.196 19.344,12.436 18.406,12.436ZM4.159,12.573C3.723,12.573 3.369,12.926 3.369,13.363C3.369,13.799 3.723,14.153 4.159,14.153H5.542C5.978,14.153 6.332,13.799 6.332,13.363C6.332,12.926 5.978,12.573 5.542,12.573H4.159ZM4.278,4.296C3.745,4.296 3.313,4.727 3.313,5.259C3.313,5.791 3.745,6.222 4.278,6.222H15.929C16.237,6.222 16.533,6.222 16.816,6.224C16.808,5.975 16.791,5.839 16.76,5.724C16.581,5.059 16.061,4.54 15.394,4.362C15.18,4.304 14.889,4.296 13.93,4.296H4.278Z">
<aapt:attr name="android:fillColor">
<gradient android:centerX="7.343" android:centerY="0.784" android:gradientRadius="33.786" android:type="radial">
<item android:color="#FF96999B" android:offset="0"/>
<item android:color="#FF4D4E53" android:offset="1"/>
</gradient>
</aapt:attr>
</path>
</vector>

View file

@ -7,6 +7,7 @@ import com.tangem.blockchainsdk.BlockchainSDKFactory
import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.data.walletmanager.extensions.makePublicKey
import com.tangem.data.walletmanager.extensions.makeWalletManagerForApp
import com.tangem.domain.card.configs.Wallet2CardConfig
import com.tangem.domain.wallets.derivations.DerivationStyleProvider
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.models.wallet.UserWallet
@ -41,7 +42,7 @@ internal class WalletManagerFactory(
blockchain: Blockchain,
derivationPath: DerivationPath?,
): WalletManager? {
val curve = blockchain.getSupportedCurves().first()
val curve = Wallet2CardConfig.primaryCurve(blockchain)
val selectedWallet = hotWallet.wallets.orEmpty().firstOrNull { it.curve == curve }
?: return null
return try {

View file

@ -46,9 +46,8 @@ class HotWalletAccessor @Inject constructor(
auth = auth,
block = { blockAuth ->
block(blockAuth).also {
// TODO [REDACTED_TASK_KEY] [Hot Wallet] Authorization by access code
// if user has biometry enabled, we set it as the new auth method
if (blockAuth is HotAuth.Password /*&& has biometry enabled */) {
// Update biometry auth if the original auth was password
if (blockAuth is HotAuth.Password) {
tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
walletId = hotWalletId,

View file

@ -25,7 +25,7 @@ class HotUserWalletBuilder @AssistedInject constructor(
) {
suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) {
val allNetworks = Blockchain.entries // TODO [REDACTED_TASK_KEY] [Hot Wallet] Derivation config for hot wallet
val allNetworks = Blockchain.entries.filter { it.isTestnet().not() }
val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet()
val requests = curves.sortedBy { it.ordinal }.map { curve ->
val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() }

View file

@ -8,4 +8,8 @@ class GetIsBiometricsEnabledUseCase @Inject constructor(
) {
operator fun invoke(): Boolean = runCatching(tangemSdkManager::needEnrollBiometrics).getOrNull() ?: false
fun canUseBiometry(): Boolean {
return tangemSdkManager.canUseBiometry
}
}

View file

@ -46,7 +46,11 @@ class SaveWalletUseCase(
UserWalletsListRepository.LockMethod.NoLock,
)
}
}.mapLeft { SaveWalletError.DataError(null) }.bind()
}.mapLeft {
SaveWalletError.DataError(null)
}.map {
userWalletsListRepository.select(userWallet.walletId)
}.bind()
}
}
} else {

View file

@ -36,6 +36,7 @@ internal class UserWalletListModel @Inject constructor(
private val userWalletsFetcher = userWalletsFetcherFactory.create(
messageSender = messageSender,
onlyMultiCurrency = false,
authMode = false,
onWalletClick = { userWalletId -> router.push(AppRoute.WalletSettings(userWalletId)) },
)

View file

@ -5,10 +5,10 @@ import arrow.core.getOrElse
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetUserWalletUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.accesscode.entity.AccessCodeUM
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotAuth
@ -27,7 +27,7 @@ internal class AccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val saveWalletUseCase: SaveWalletUseCase,
private val userWalletsListRepository: UserWalletsListRepository,
private val tangemHotSdk: TangemHotSdk,
) : Model() {
@ -77,15 +77,41 @@ internal class AccessCodeModel @Inject constructor(
runCatching {
val userWallet = getUserWalletUseCase(userWalletId)
.getOrElse { error("User wallet with id $userWalletId not found") }
if (userWallet is UserWallet.Hot) {
val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth)
val updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = unlockHotWallet,
if (userWallet !is UserWallet.Hot) return@launch
val unlockHotWallet = UnlockHotWallet(userWallet.hotWalletId, HotAuth.NoAuth)
var updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = unlockHotWallet,
auth = HotAuth.Password(accessCode.toCharArray()),
)
updatedHotWalletId = tangemHotSdk.changeAuth(
unlockHotWallet = UnlockHotWallet(
walletId = updatedHotWalletId,
auth = HotAuth.Password(accessCode.toCharArray()),
)
saveWalletUseCase(userWallet.copy(hotWalletId = updatedHotWalletId), canOverride = true)
params.callbacks.onAccessCodeConfirmed(params.userWalletId)
}
),
auth = HotAuth.Biometry,
)
userWalletsListRepository.saveWithoutLock(
userWallet.copy(
hotWalletId = updatedHotWalletId,
backedUp = true,
),
canOverride = true,
)
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.AccessCode(accessCode.toCharArray()),
)
userWalletsListRepository.setLock(
userWallet.walletId,
UserWalletsListRepository.LockMethod.Biometric,
)
params.callbacks.onAccessCodeConfirmed(params.userWalletId)
}.onFailure {
Timber.e(it)

View file

@ -45,9 +45,8 @@ internal class CreateMobileWalletModel @Inject constructor(
runCatching {
val hotWalletId = tangemHotSdk.generateWallet(HotAuth.NoAuth, mnemonicType = MnemonicType.Words12)
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
saveUserWalletUseCase(
hotUserWalletBuilder.build(),
)
val userWallet = hotUserWalletBuilder.build()
saveUserWalletUseCase(userWallet)
router.replaceAll(AppRoute.Wallet)
}.onFailure {
Timber.e(it)

View file

@ -14,6 +14,7 @@ interface UserWalletsFetcher {
fun create(
messageSender: UiMessageSender,
onlyMultiCurrency: Boolean,
authMode: Boolean,
onWalletClick: (UserWalletId) -> Unit,
): UserWalletsFetcher
}

View file

@ -43,7 +43,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
private val getBalanceHidingSettingsUseCase: GetBalanceHidingSettingsUseCase,
@Assisted private val onWalletClick: (UserWalletId) -> Unit,
@Assisted private val messageSender: UiMessageSender,
@Assisted private val onlyMultiCurrency: Boolean,
@Assisted("onlyMultiCurrency") private val onlyMultiCurrency: Boolean,
@Assisted("authMode") private val authMode: Boolean,
private val getCardImageUseCase: GetCardImageUseCase,
dispatchers: CoroutineDispatcherProvider,
) : UserWalletsFetcher {
@ -54,7 +55,10 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
@OptIn(ExperimentalCoroutinesApi::class)
override val userWallets: Flow<ImmutableList<UserWalletItemUM>> = walletsFlow.transformLatest { wallets ->
val uiModels = UserWalletItemUMConverter(onClick = onWalletClick).convertList(wallets)
val uiModels = UserWalletItemUMConverter(
onClick = onWalletClick,
authMode = authMode,
).convertList(wallets)
.toImmutableList()
emit(uiModels)
@ -132,6 +136,7 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
balance = balance,
isBalanceHidden = balanceHidingSettings.isBalanceHidden,
artwork = artworks[userWallet.walletId],
authMode = authMode,
)
.convert(userWallet)
}
@ -149,7 +154,8 @@ internal class DefaultUserWalletsFetcher @AssistedInject constructor(
interface Factory : UserWalletsFetcher.Factory {
override fun create(
messageSender: UiMessageSender,
onlyMultiCurrency: Boolean,
@Assisted("onlyMultiCurrency") onlyMultiCurrency: Boolean,
@Assisted("authMode") authMode: Boolean,
onWalletClick: (UserWalletId) -> Unit,
): DefaultUserWalletsFetcher
}

View file

@ -29,6 +29,7 @@ internal class WcUserWalletsFetcher(
private val userWalletsFetcher = userWalletsFetcherFactory.create(
messageSender = messageSender,
onlyMultiCurrency = true,
authMode = false,
onWalletClick = { onWalletSelected(it) },
)

View file

@ -13,11 +13,13 @@ android {
dependencies {
implementation(projects.features.welcome.api)
implementation(projects.features.wallet.api)
/** Core */
implementation(projects.core.configToggles)
implementation(projects.core.decompose)
implementation(projects.core.ui)
implementation(projects.core.analytics)
implementation(projects.common.routing)
implementation(projects.common.ui)
@ -30,6 +32,7 @@ dependencies {
/** Domain */
implementation(projects.domain.appCurrency)
implementation(projects.domain.wallets)
implementation(projects.domain.card)
/** DI */
implementation(deps.hilt.android)
@ -54,4 +57,5 @@ dependencies {
implementation(deps.timber)
implementation(tangemDeps.card.core)
implementation(tangemDeps.blockchain)
implementation(tangemDeps.hot.core)
}

View file

@ -1,18 +1,233 @@
package com.tangem.features.welcome.impl.model
import com.tangem.common.routing.AppRoute
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.navigation.Router
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.message.SnackbarMessage
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.isLocked
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.core.wallets.error.UnlockWalletError
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.domain.wallets.usecase.GetIsBiometricsEnabledUseCase
import com.tangem.features.wallet.utils.UserWalletsFetcher
import com.tangem.features.welcome.impl.R
import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM
import com.tangem.features.welcome.impl.ui.state.AddWalletBottomSheetContentUM.Option.*
import com.tangem.features.welcome.impl.ui.state.WelcomeUM
import com.tangem.hot.sdk.model.HotWalletId
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.coroutines.JobHolder
import com.tangem.utils.coroutines.saveIn
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
@ModelScoped
internal class WelcomeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val router: Router,
private val uiMessageSender: UiMessageSender,
private val userWalletsFetcherFactory: UserWalletsFetcher.Factory,
private val userWalletsListRepository: UserWalletsListRepository,
private val getIsBiometricsEnabledUseCase: GetIsBiometricsEnabledUseCase,
) : Model() {
// TODO add intent handling
// val params
val uiState: StateFlow<WelcomeUM>
field = MutableStateFlow(WelcomeUM.Plain)
field = MutableStateFlow<WelcomeUM>(WelcomeUM.Plain)
private val walletsFetcher = userWalletsFetcherFactory.create(
messageSender = uiMessageSender,
onlyMultiCurrency = false,
authMode = true,
onWalletClick = { walletId ->
modelScope.launch {
val userWallets = userWalletsListRepository.userWalletsSync()
val userWallet = userWallets.first { it.walletId == walletId }
onUserWalletClick(userWallet)
}
},
)
private val walletsFetcherJobHolder = JobHolder()
private val wallets = MutableStateFlow<ImmutableList<UserWalletItemUM>>(persistentListOf())
init {
modelScope.launch {
userWalletsListRepository.load()
wallets.value = walletsFetcher.userWallets.first()
launch {
walletsFetcher.userWallets
.collectLatest { wallets.value = it }
}
tryToUnlockRightAway()
}
}
private fun tryToUnlockRightAway() {
modelScope.launch {
if (canUnlockWithBiometrics()) {
userWalletsListRepository.unlockAllWallets()
.onRight {
router.replaceAll(AppRoute.Wallet)
}
.onLeft {
it.handle(null, onUserCancelled = { tryToUnlockWithAccessCodeRightAway() })
setSelectWalletState()
}
} else {
tryToUnlockWithAccessCodeRightAway()
setSelectWalletState()
}
}
}
private fun tryToUnlockWithAccessCodeRightAway() = modelScope.launch {
if (onlyOneHotWalletWithAccessCode()) {
val userWallets = userWalletsListRepository.userWalletsSync()
val userWallet = userWallets.first()
unlockWallet(userWallet.walletId, UserWalletsListRepository.UnlockMethod.AccessCode)
}
}
private fun setSelectWalletState() {
modelScope.launch {
uiState.value = WelcomeUM.SelectWallet(
wallets = walletsFetcher.userWallets.first(),
showUnlockWithBiometricButton = canUnlockWithBiometrics(),
addWalletClick = ::addWalletClick,
onUnlockWithBiometricClick = {
modelScope.launch {
userWalletsListRepository.unlockAllWallets()
.onRight {
router.replaceAll(AppRoute.Wallet)
}
.onLeft {
it.handle(null, onUserCancelled = { /* ignore */ })
}
}
},
)
wallets.collectLatest { wallets ->
updateSelectState {
it.copy(wallets = wallets)
}
}
}.saveIn(walletsFetcherJobHolder)
}
private fun addWalletClick() {
updateSelectState { currentState ->
currentState.copy(
addWalletBottomSheet = TangemBottomSheetConfig(
isShown = true,
content = AddWalletBottomSheetContentUM(
onOptionClick = ::onAddWalletOptionClick,
),
onDismissRequest = {
updateSelectState {
it.copy(addWalletBottomSheet = it.addWalletBottomSheet.copy(isShown = false))
}
},
),
)
}
}
private fun onAddWalletOptionClick(option: AddWalletBottomSheetContentUM.Option) {
when (option) {
Create -> router.push(AppRoute.CreateWalletSelection)
Add -> router.push(AppRoute.AddExistingWallet)
Buy -> {
}
}
}
private suspend fun onlyOneHotWalletWithAccessCode(): Boolean {
val userWalletsWithLock = userWalletsListRepository.userWalletsSync().filter { it.isLocked }
if (userWalletsWithLock.size != 1) return false
val wallet = userWalletsWithLock.first()
return wallet is UserWallet.Hot && wallet.hotWalletId.authType != HotWalletId.AuthType.NoPassword
}
private fun onUserWalletClick(userWallet: UserWallet) = modelScope.launch {
if (userWallet.isLocked.not()) {
// If the wallet is not locked, we can proceed to the wallet screen directly
userWalletsListRepository.select(userWallet.walletId)
router.replaceAll(AppRoute.Wallet)
return@launch
}
val unlockMethod = when (userWallet) {
is UserWallet.Cold -> UserWalletsListRepository.UnlockMethod.Scan
is UserWallet.Hot -> UserWalletsListRepository.UnlockMethod.AccessCode
}
unlockWallet(userWallet.walletId, unlockMethod)
}
private fun canUnlockWithBiometrics(): Boolean {
return getIsBiometricsEnabledUseCase.canUseBiometry()
}
suspend fun unlockWallet(userWalletId: UserWalletId, unlockMethod: UserWalletsListRepository.UnlockMethod) {
userWalletsListRepository.unlock(userWalletId, unlockMethod)
.onRight {
userWalletsListRepository.select(userWalletId)
router.replaceAll(AppRoute.Wallet)
}
.onLeft { error ->
error.handle(specificWalletId = userWalletId, onUserCancelled = { /* ignore*/ })
}
}
suspend fun UnlockWalletError.handle(specificWalletId: UserWalletId?, onUserCancelled: () -> Unit = { }) {
when (this) {
UnlockWalletError.AlreadyUnlocked -> {
// this should not happen, as we check for locked state before this
specificWalletId?.let { userWalletsListRepository.select(it) }
router.replaceAll(AppRoute.Wallet)
}
UnlockWalletError.ScannedCardWalletNotMatched -> {
// TODO Scanned card does not match the wallet
}
UnlockWalletError.UnableToUnlock -> {
// TODO Unable to unlock the wallet"
}
UnlockWalletError.UserCancelled -> onUserCancelled()
UnlockWalletError.UserWalletNotFound -> {
// This should never happen in this flow, as we always check for the wallet existence before unlocking
Timber.e("User wallet not found for unlock: $specificWalletId")
uiMessageSender.send(
SnackbarMessage(TextReference.Res(R.string.generic_error)),
)
}
}
}
private fun updateSelectState(block: (WelcomeUM.SelectWallet) -> WelcomeUM.SelectWallet) {
uiState.update { currentState ->
if (currentState is WelcomeUM.SelectWallet) {
block(currentState)
} else {
currentState
}
}
}
}

View file

@ -10,10 +10,11 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.extensions.TextReference
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.welcome.impl.ui.state.WalletUM
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.features.welcome.impl.ui.state.WelcomeUM
import kotlinx.collections.immutable.persistentListOf
@ -34,10 +35,6 @@ internal fun Welcome(state: WelcomeUM, modifier: Modifier = Modifier) {
state = st,
modifier = modifier,
)
is WelcomeUM.EnterAccessCode -> WelcomeEnterAccessCode(
state = st,
modifier = modifier,
)
}
}
}
@ -49,22 +46,30 @@ private fun Preview() {
TangemThemePreview {
val state = WelcomeUM.SelectWallet(
wallets = persistentListOf(
WalletUM(
name = TextReference.Str("Wallet 1"),
subtitle = TextReference.Str("3 cards"),
imageState = WalletUM.ImageState.Loading,
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Loading,
balance = UserWalletItemUM.Balance.Loaded(
value = "1.2345 BTC",
isFlickering = false,
),
isEnabled = true,
onClick = {},
),
WalletUM(
name = TextReference.Str("Wallet 1"),
subtitle = TextReference.Str("Mobile wallet"),
imageState = WalletUM.ImageState.MobileWallet,
UserWalletItemUM(
id = UserWalletId("user_wallet_3".encodeToByteArray()),
name = stringReference("Multi Card"),
information = UserWalletItemUM.Information.Failed,
imageState = UserWalletItemUM.ImageState.MobileWallet,
balance = UserWalletItemUM.Balance.Locked,
isEnabled = true,
onClick = {},
),
),
)
var currentState by remember { mutableStateOf<WelcomeUM>(WelcomeUM.EnterAccessCode()) }
var currentState by remember { mutableStateOf<WelcomeUM>(WelcomeUM.SelectWallet()) }
Box {
Welcome(currentState)
@ -74,11 +79,7 @@ private fun Preview() {
onClick = {
currentState = when (currentState) {
is WelcomeUM.Plain -> state
is WelcomeUM.SelectWallet -> WelcomeUM.EnterAccessCode(
value = "",
onValueChange = {},
)
is WelcomeUM.EnterAccessCode -> WelcomeUM.Plain
is WelcomeUM.SelectWallet -> WelcomeUM.Plain
}
},
) {

View file

@ -1,94 +0,0 @@
package com.tangem.features.welcome.impl.ui
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
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.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.TopAppBarButton
import com.tangem.core.ui.components.appbar.models.TopAppBarButtonUM
import com.tangem.core.ui.components.fields.PinTextField
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.welcome.impl.ui.state.WelcomeUM
@Suppress("MagicNumber")
@Composable
internal fun AnimatedContentScope.WelcomeEnterAccessCode(
state: WelcomeUM.EnterAccessCode,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.fillMaxSize()
.statusBarsPadding(),
) {
Column {
TopAppBarButton(
modifier = Modifier
.padding(12.dp),
button = TopAppBarButtonUM.Back(onBackClicked = state.onBackClick),
tint = TangemTheme.colors.icon.primary1,
)
SpacerH(68.dp)
Text(
modifier = Modifier
.animateEnterExit(
enter = slideInVertically(
tween(delayMillis = 300),
initialOffsetY = { it + 200 },
) + fadeIn(tween(delayMillis = 300)),
exit = fadeOut(),
)
.align(Alignment.CenterHorizontally),
text = "Enter Access Code",
style = TangemTheme.typography.h2,
color = TangemTheme.colors.text.primary1,
)
SpacerH24()
Box(
modifier = Modifier
.animateEnterExit(
enter = slideInVertically(
tween(delayMillis = 300),
initialOffsetY = { it + 200 },
) + fadeIn(tween(delayMillis = 300)),
exit = fadeOut(),
)
.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
PinTextField(
length = 6,
isPasswordVisual = true,
value = state.value,
onValueChange = state.onValueChange,
)
}
}
SecondaryButton(
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(16.dp)
.navigationBarsPadding()
.imePadding()
.animateEnterExit(fadeIn(), fadeOut()),
text = "Log in with biometric",
onClick = state.onUnlockWithBiometricClick,
)
}
}

View file

@ -9,8 +9,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.welcome.impl.R
@Composable
@ -26,4 +28,14 @@ internal fun WelcomePlain(modifier: Modifier = Modifier) {
contentDescription = null,
)
}
}
@Preview(showBackground = true)
@Composable
private fun Preview() {
TangemThemePreview {
WelcomePlain(
modifier = Modifier.fillMaxSize(),
)
}
}

View file

@ -5,12 +5,9 @@ import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.*
@ -20,14 +17,13 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import com.tangem.common.ui.userwallet.CardImage
import com.tangem.common.ui.userwallet.UserWalletItem
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.*
import com.tangem.core.ui.components.block.TangemBlockCardColors
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.features.welcome.impl.R
import com.tangem.features.welcome.impl.ui.state.WalletUM
import com.tangem.features.welcome.impl.ui.state.WelcomeUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -45,7 +41,7 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal
TitleText()
SpacerH12()
var actualWallets by remember { mutableStateOf<ImmutableList<WalletUM>>(persistentListOf()) }
var actualWallets by remember { mutableStateOf<ImmutableList<UserWalletItemUM>>(persistentListOf()) }
Box(modifier = Modifier.weight(1f)) {
LazyColumn(
@ -62,9 +58,12 @@ internal fun AnimatedContentScope.WelcomeSelectWallet(state: WelcomeUM.SelectWal
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
itemsIndexed(actualWallets) { index, walletState ->
WalletItem(
UserWalletItem(
modifier = Modifier.fillMaxWidth(),
state = walletState,
modifier = Modifier,
blockColors = TangemBlockCardColors.copy(
containerColor = TangemTheme.colors.field.primary,
),
)
}
}
@ -165,66 +164,4 @@ private fun AnimatedContentScope.TitleText(modifier: Modifier = Modifier) {
color = TangemTheme.colors.text.secondary,
)
}
}
@Suppress("MagicNumber")
@Composable
private fun WalletItem(state: WalletUM, modifier: Modifier = Modifier) {
Row(
modifier = modifier
.fillMaxWidth()
.clip(TangemTheme.shapes.roundedCornersXMedium)
.background(TangemTheme.colors.background.secondary, TangemTheme.shapes.roundedCornersXMedium)
.clickable(onClick = state.onClick)
.padding(12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
WalletImage(state.imageState)
SpacerW12()
Column(Modifier.weight(1f)) {
Text(
text = state.name.resolveReference(),
style = TangemTheme.typography.subtitle1,
color = TangemTheme.colors.text.primary1,
)
Text(
text = state.subtitle.resolveReference(),
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.tertiary,
)
}
}
}
@Composable
private fun WalletImage(state: WalletUM.ImageState, modifier: Modifier = Modifier) {
when (state) {
WalletUM.ImageState.MobileWallet -> {
Box(
modifier = modifier
.size(36.dp)
.background(TangemTheme.colors.icon.accent.copy(alpha = 0.1f), CircleShape),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_wallet_filled_24),
tint = TangemTheme.colors.icon.accent,
contentDescription = null,
)
}
}
else -> {
CardImage(
imageState = when (state) {
is WalletUM.ImageState.Image -> UserWalletItemUM.ImageState.Image(state.artwork)
WalletUM.ImageState.Loading -> UserWalletItemUM.ImageState.Loading
else -> error("")
},
modifier = modifier,
)
}
}
}

View file

@ -1,22 +0,0 @@
package com.tangem.features.welcome.impl.ui.state
import com.tangem.core.ui.components.artwork.ArtworkUM
import com.tangem.core.ui.extensions.TextReference
import javax.annotation.concurrent.Immutable
internal data class WalletUM(
val name: TextReference,
val subtitle: TextReference,
val imageState: ImageState,
val onClick: () -> Unit,
) {
@Immutable
sealed class ImageState {
data object MobileWallet : ImageState()
data object Loading : ImageState()
data class Image(
val artwork: ArtworkUM,
) : ImageState()
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.features.welcome.impl.ui.state
import androidx.compose.runtime.Immutable
import com.tangem.common.ui.userwallet.state.UserWalletItemUM
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@ -11,17 +12,10 @@ internal sealed class WelcomeUM {
data object Plain : WelcomeUM()
data class SelectWallet(
val wallets: ImmutableList<WalletUM> = persistentListOf(),
val wallets: ImmutableList<UserWalletItemUM> = persistentListOf(),
val showUnlockWithBiometricButton: Boolean = false,
val addWalletBottomSheet: TangemBottomSheetConfig = TangemBottomSheetConfig.Empty,
val onUnlockWithBiometricClick: () -> Unit = {},
val addWalletClick: () -> Unit = {},
) : WelcomeUM()
data class EnterAccessCode(
val value: String = "",
val onUnlockWithBiometricClick: () -> Unit = {},
val onValueChange: (String) -> Unit = {},
val onBackClick: () -> Unit = {},
) : WelcomeUM()
}

@ -1 +1 @@
Subproject commit 794a8187e6d248ca3c21661df199a34ffeb0037a
Subproject commit bc4cd430853ca794614b8d5163c9b28b9ca26112