Updated on 2026-08-14

This commit is contained in:
Tangem 2025-09-10 16:15:30 +03:00
commit 2f62d482ca
635 changed files with 18802 additions and 9118 deletions

View file

@ -13,12 +13,14 @@ import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.core.wallets.UserWalletsListRepository
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.settings.repositories.SettingsRepository
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.legacy.asLockable
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.features.biometry.AskBiometryComponent
import com.tangem.features.hotwallet.HotWalletFeatureToggles
import com.tangem.features.onboarding.v2.TitleProvider
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
import com.tangem.features.onboarding.v2.done.api.OnboardingDoneComponent
@ -46,6 +48,8 @@ internal class OnboardingEntryModel @Inject constructor(
private val analyticsEventHandler: AnalyticsEventHandler,
private val uiMessageSender: UiMessageSender,
private val userWalletsListManager: UserWalletsListManager,
private val hotWalletFeatureToggles: HotWalletFeatureToggles,
private val userWalletsListRepository: UserWalletsListRepository,
) : Model() {
private val params = paramsContainer.require<OnboardingEntryComponent.Params>()
@ -145,7 +149,7 @@ internal class OnboardingEntryModel @Inject constructor(
doneMode: OnboardingDoneComponent.Mode = OnboardingDoneComponent.Mode.WalletCreated,
) {
modelScope.launch {
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowSaveUserWalletScreen()) {
if (tangemSdkManager.checkCanUseBiometry() && settingsRepository.shouldShowAskBiometry()) {
doIfVisa {
analyticsEventHandler.send(OnboardingVisaAnalyticsEvent.BiometricScreenOpened)
}
@ -197,6 +201,19 @@ internal class OnboardingEntryModel @Inject constructor(
}
private fun exitComponentScreen() {
// new flow
if (hotWalletFeatureToggles.isHotWalletEnabled) {
modelScope.launch {
if (userWalletsListRepository.userWalletsSync().isEmpty()) {
router.replaceAll(AppRoute.Home())
} else {
router.replaceAll(AppRoute.Wallet)
}
}
return
}
// legacy flow
if (userWalletsListManager.hasUserWallets) {
val isLocked = runCatching { userWalletsListManager.asLockable()?.isLockedSync!! }.getOrElse { false }

View file

@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.ScanResponse
@ -39,7 +39,7 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val cardRepository: CardRepository,
private val analyticsHandler: AnalyticsEventHandler,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
@ -168,7 +168,8 @@ internal class MultiWalletCreateWalletModel @Inject constructor(
fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
val cardInfo =
getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}

View file

@ -9,7 +9,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.decompose.ui.UiMessageSender
import com.tangem.domain.card.common.util.cardTypesResolver
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.domain.models.scan.CardDTO
@ -19,8 +19,8 @@ import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.models.wallet.requireColdWallet
import com.tangem.domain.onboarding.repository.OnboardingRepository
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.wallets.legacy.UserWalletsListManager
import com.tangem.domain.wallets.repository.WalletsRepository
import com.tangem.domain.wallets.usecase.GetWalletsUseCase
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.domain.wallets.usecase.UpdateWalletUseCase
import com.tangem.features.onboarding.v2.common.ui.CantLeaveBackupDialog
@ -37,7 +37,10 @@ import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.StringsSigns
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@ -49,11 +52,11 @@ internal class MultiWalletFinalizeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val backupServiceHolder: BackupServiceHolder,
private val tangemSdkManager: TangemSdkManager,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
private val userWalletsListManager: UserWalletsListManager,
private val saveWalletUseCase: SaveWalletUseCase,
private val getUserWalletsUseCase: GetWalletsUseCase,
private val updateWalletUseCase: UpdateWalletUseCase,
private val cardRepository: CardRepository,
private val onboardingRepository: OnboardingRepository,
@ -244,7 +247,7 @@ internal class MultiWalletFinalizeModel @Inject constructor(
userWalletCreated
}
OnboardingMultiWalletComponent.Mode.AddBackup -> {
val userWallet = userWalletsListManager.userWallets.first()
val userWallet = getUserWalletsUseCase.invokeSync()
.firstOrNull {
it is UserWallet.Cold &&
it.scanResponse.primaryCard?.cardId == scanResponse.primaryCard?.cardId
@ -333,7 +336,8 @@ internal class MultiWalletFinalizeModel @Inject constructor(
private fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
val cardInfo =
getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}

View file

@ -10,7 +10,7 @@ import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.navigation.url.UrlOpener
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.domain.card.repository.CardRepository
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.features.hotwallet.MnemonicRepository
@ -44,7 +44,7 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
private val urlOpener: UrlOpener,
private val tangemSdkManager: TangemSdkManager,
private val cardRepository: CardRepository,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val analyticsHandler: AnalyticsEventHandler,
) : Model() {
@ -257,7 +257,8 @@ internal class MultiWalletSeedPhraseModel @Inject constructor(
fun navigateToSupportScreen() {
modelScope.launch {
val cardInfo = getCardInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
val cardInfo =
getWalletMetaInfoUseCase(multiWalletState.value.currentScanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(FeedbackEmailType.DirectUserRequest(cardInfo))
}
}

View file

@ -1,5 +1,6 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.builder
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
@ -20,10 +21,10 @@ internal class GenerateSeedPhraseUiStateBuilder(
option: GeneratedWordsType,
): MultiWalletSeedPhraseUM.GenerateSeedPhrase {
val words12 = generatedWords12.mnemonicComponents.mapIndexed { index, s ->
MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s)
EnumeratedTwoColumnGridItem(index + 1, s)
}.toImmutableList()
val words24 = generatedWords24.mnemonicComponents.mapIndexed { index, s ->
MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem(index + 1, s)
EnumeratedTwoColumnGridItem(index + 1, s)
}.toImmutableList()
return MultiWalletSeedPhraseUM.GenerateSeedPhrase(
@ -45,8 +46,8 @@ internal class GenerateSeedPhraseUiStateBuilder(
private fun switchType(
newType: GeneratedWordsType,
generatedWords12: ImmutableList<MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem>,
generatedWords24: ImmutableList<MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem>,
generatedWords12: ImmutableList<EnumeratedTwoColumnGridItem>,
generatedWords24: ImmutableList<EnumeratedTwoColumnGridItem>,
) {
updateUiState { uiSt ->
changeGeneratedWordsType(newType)

View file

@ -5,15 +5,14 @@ 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.components.buttons.segmentedbutton.SegmentedButtons
import com.tangem.core.ui.components.grid.EnumeratedTwoColumnGrid
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -21,8 +20,6 @@ import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.features.onboarding.v2.impl.R
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.state.MultiWalletSeedPhraseUM.GenerateSeedPhrase.MnemonicGridItem
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@ -45,8 +42,8 @@ internal fun MultiWalletSeedPhraseWords(
TitleBlock(state)
SeedPhraseGridBlock(
mnemonicGridItems = state.words,
EnumeratedTwoColumnGrid(
items = state.words,
modifier = Modifier
.fillMaxWidth()
.padding(top = 20.dp, bottom = 32.dp),
@ -127,67 +124,6 @@ private fun TitleBlock(state: MultiWalletSeedPhraseUM.GenerateSeedPhrase, modifi
}
}
@Composable
private fun SeedPhraseGridBlock(mnemonicGridItems: ImmutableList<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, heightDp = 640)
@Composable
private fun Preview() {
@ -195,7 +131,7 @@ private fun Preview() {
MultiWalletSeedPhraseWords(
state = MultiWalletSeedPhraseUM.GenerateSeedPhrase(
words = List(24) {
MnemonicGridItem(
EnumeratedTwoColumnGridItem(
index = it + 1,
mnemonic = "word1",
)

View file

@ -3,6 +3,7 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui.s
import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.input.TextFieldValue
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.grid.entity.EnumeratedTwoColumnGridItem
import com.tangem.core.ui.extensions.TextReference
import com.tangem.features.onboarding.v2.common.ui.OnboardingDialogUM
import com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.model.GeneratedWordsType
@ -22,15 +23,10 @@ internal sealed class MultiWalletSeedPhraseUM(
data class GenerateSeedPhrase(
val option: GeneratedWordsType = GeneratedWordsType.Words12,
val words: ImmutableList<MnemonicGridItem> = persistentListOf(),
val words: ImmutableList<EnumeratedTwoColumnGridItem> = persistentListOf(),
val onOptionChange: (GeneratedWordsType) -> Unit = {},
val onContinueClick: () -> Unit = {},
) : MultiWalletSeedPhraseUM(order = 1) {
data class MnemonicGridItem(
val index: Int,
val mnemonic: String,
)
}
) : MultiWalletSeedPhraseUM(order = 1)
data class GeneratedWordsCheck(
val wordFields: ImmutableList<WordField> = persistentListOf(),

View file

@ -10,7 +10,7 @@ import com.tangem.core.analytics.api.AnalyticsEventHandler
import com.tangem.core.analytics.models.AnalyticsEvent
import com.tangem.core.decompose.context.AppComponentContext
import com.tangem.domain.card.common.TapWorkarounds.isVisa
import com.tangem.domain.feedback.GetCardInfoUseCase
import com.tangem.domain.feedback.GetWalletMetaInfoUseCase
import com.tangem.domain.feedback.SendFeedbackEmailUseCase
import com.tangem.domain.feedback.models.FeedbackEmailType
import com.tangem.features.onboarding.v2.stepper.api.OnboardingStepperComponent
@ -24,7 +24,7 @@ import kotlinx.coroutines.launch
internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
@Assisted val context: AppComponentContext,
@Assisted val params: OnboardingStepperComponent.Params,
private val getCardInfoUseCase: GetCardInfoUseCase,
private val getWalletMetaInfoUseCase: GetWalletMetaInfoUseCase,
private val sendFeedbackEmailUseCase: SendFeedbackEmailUseCase,
private val analyticsHandler: AnalyticsEventHandler,
) : OnboardingStepperComponent, AppComponentContext by context {
@ -40,7 +40,7 @@ internal class DefaultOnboardingStepperComponent @AssistedInject constructor(
)
componentScope.launch {
val cardInfo = getCardInfoUseCase(params.scanResponse).getOrNull() ?: return@launch
val cardInfo = getWalletMetaInfoUseCase(params.scanResponse).getOrNull() ?: return@launch
sendFeedbackEmailUseCase(
if (params.scanResponse.card.isVisa) {
FeedbackEmailType.Visa.Activation(cardInfo)

View file

@ -18,7 +18,7 @@ import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.model.VisaCustomerWalletDataToSignRequest
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.OnboardingVisaAccessCodeComponent
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
import com.tangem.features.onboarding.v2.visa.impl.child.welcome.model.analytics.OnboardingVisaAnalyticsEvent
@ -44,7 +44,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
@Suppress("UnusedPrivateMember")
private val tangemSdkManager: TangemSdkManager,
private val visaAuthRepository: VisaAuthRepository,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val uiMessageSender: UiMessageSender,
private val analyticsEventsHandler: AnalyticsEventHandler,
) : Model() {
@ -155,7 +155,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
loading(true)
modelScope.launch {
val challengeToSign = visaAuthRepository.getCardAuthChallenge(
val challengeToSign = visaAuthRemoteDataSource.getCardAuthChallenge(
cardId = activationInput.cardId,
cardPublicKey = activationInput.cardPublicKey,
).getOrElse {

View file

@ -18,7 +18,7 @@ import com.tangem.domain.visa.model.VisaAuthTokens
import com.tangem.domain.visa.model.VisaCardActivationStatus
import com.tangem.domain.visa.model.VisaCardId
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.domain.visa.datasource.VisaAuthRemoteDataSource
import com.tangem.domain.wallets.builder.ColdUserWalletBuilder
import com.tangem.domain.models.wallet.UserWallet
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
@ -42,7 +42,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
paramsContainer: ParamsContainer,
visaActivationRepositoryFactory: VisaActivationRepository.Factory,
override val dispatchers: CoroutineDispatcherProvider,
private val visaAuthRepository: VisaAuthRepository,
private val visaAuthRemoteDataSource: VisaAuthRemoteDataSource,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val otpStorage: VisaOTPStorage,
private val coldUserWalletBuilderFactory: ColdUserWalletBuilder.Factory,
@ -166,7 +166,7 @@ internal class OnboardingVisaInProgressModel @Inject constructor(
val authTokens = visaAuthTokenStorage.get(params.scanResponse.card.cardId)
?: error("Auth tokens are not found. This should not happen.")
val newTokens = visaAuthRepository.exchangeAccessToken(authTokens)
val newTokens = visaAuthRemoteDataSource.exchangeAccessToken(authTokens)
.getOrElse {
uiMessageSender.showErrorDialog(it)
return