Updated on 2026-08-14

This commit is contained in:
Tangem 2025-08-01 09:53:17 +05:00
parent 48dc308f31
commit 02f6e52f46
14 changed files with 244 additions and 197 deletions

View file

@ -9,6 +9,7 @@ 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.models.wallet.UserWalletId
import com.tangem.domain.settings.ShouldAskPermissionUseCase
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.entry.routing.AddExistingWalletRoute
@ -78,23 +79,23 @@ internal class AddExistingWalletModel @Inject constructor(
}
inner class AddExistingWalletImportModelCallbacks : AddExistingWalletImportComponent.ModelCallbacks {
override fun onWalletImported() {
stackNavigation.replaceCurrent(AddExistingWalletRoute.BackupCompleted)
override fun onWalletImported(userWalletId: UserWalletId) {
stackNavigation.replaceCurrent(AddExistingWalletRoute.BackupCompleted(userWalletId))
}
}
inner class ManualBackupCompletedComponentModelCallbacks : ManualBackupCompletedComponent.ModelCallbacks {
override fun onContinueClick() {
stackNavigation.replaceCurrent(AddExistingWalletRoute.SetAccessCode)
override fun onContinueClick(userWalletId: UserWalletId) {
stackNavigation.replaceCurrent(AddExistingWalletRoute.SetAccessCode(userWalletId))
}
}
inner class AccessCodeModelCallbacks : AccessCodeComponent.ModelCallbacks {
override fun onAccessCodeSet(accessCode: String) {
stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(accessCode))
override fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String) {
stackNavigation.push(AddExistingWalletRoute.ConfirmAccessCode(userWalletId, accessCode))
}
override fun onAccessCodeConfirmed() {
override fun onAccessCodeConfirmed(userWalletId: UserWalletId) {
navigateToPushNotificationsOrNext()
}
}

View file

@ -37,6 +37,7 @@ internal class AddExistingWalletChildFactory @Inject constructor(
is AddExistingWalletRoute.BackupCompleted -> ManualBackupCompletedComponent(
context = childContext,
params = ManualBackupCompletedComponent.Params(
userWalletId = route.userWalletId,
callbacks = model.manualBackupCompletedComponentModelCallbacks,
),
)
@ -44,6 +45,7 @@ internal class AddExistingWalletChildFactory @Inject constructor(
context = childContext,
params = AccessCodeComponent.Params(
isConfirmMode = false,
userWalletId = route.userWalletId,
callbacks = model.accessCodeModelCallbacks,
),
)
@ -52,6 +54,7 @@ internal class AddExistingWalletChildFactory @Inject constructor(
params = AccessCodeComponent.Params(
isConfirmMode = true,
accessCodeToConfirm = route.accessCode,
userWalletId = route.userWalletId,
callbacks = model.accessCodeModelCallbacks,
),
)

View file

@ -1,6 +1,7 @@
package com.tangem.features.hotwallet.addexistingwallet.entry.routing
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.models.wallet.UserWalletId
import kotlinx.serialization.Serializable
internal sealed class AddExistingWalletRoute : Route {
@ -12,13 +13,13 @@ internal sealed class AddExistingWalletRoute : Route {
object Import : AddExistingWalletRoute()
@Serializable
object BackupCompleted : AddExistingWalletRoute()
data class BackupCompleted(val userWalletId: UserWalletId) : AddExistingWalletRoute()
@Serializable
object SetAccessCode : AddExistingWalletRoute()
data class SetAccessCode(val userWalletId: UserWalletId) : AddExistingWalletRoute()
@Serializable
data class ConfirmAccessCode(val accessCode: String) : AddExistingWalletRoute()
data class ConfirmAccessCode(val userWalletId: UserWalletId, val accessCode: String) : AddExistingWalletRoute()
@Serializable
object PushNotifications : AddExistingWalletRoute()

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.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.addexistingwallet.im.port.model.AddExistingWalletImportModel
import com.tangem.features.hotwallet.addexistingwallet.im.port.ui.AddExistingWalletImportContent
import dagger.assisted.Assisted
@ -28,7 +29,7 @@ internal class AddExistingWalletImportComponent @AssistedInject constructor(
}
interface ModelCallbacks {
fun onWalletImported()
fun onWalletImported(userWalletId: UserWalletId)
}
data class Params(

View file

@ -4,13 +4,19 @@ import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.crypto.bip39.Mnemonic
import com.tangem.domain.wallets.builder.HotUserWalletBuilder
import com.tangem.domain.wallets.usecase.SaveWalletUseCase
import com.tangem.features.hotwallet.MnemonicRepository
import com.tangem.features.hotwallet.addexistingwallet.im.port.AddExistingWalletImportComponent
import com.tangem.features.hotwallet.addexistingwallet.im.port.entity.AddExistingWalletImportUM
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotAuth
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
@ -18,6 +24,9 @@ internal class AddExistingWalletImportModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val mnemonicRepository: MnemonicRepository,
private val tangemHotSdk: TangemHotSdk,
private val hotUserWalletBuilderFactory: HotUserWalletBuilder.Factory,
private val saveUserWalletUseCase: SaveWalletUseCase,
) : Model() {
private val params: AddExistingWalletImportComponent.Params = paramsContainer.require()
@ -44,7 +53,24 @@ internal class AddExistingWalletImportModel @Inject constructor(
@Suppress("UnusedPrivateMember")
private fun importWallet(mnemonic: Mnemonic, passphrase: String?) {
// TODO implement importing seed phrase
params.callbacks.onWalletImported()
modelScope.launch {
uiState.update {
it.copy(createWalletProgress = true)
}
runCatching {
val hotWalletId = tangemHotSdk.importWallet(mnemonic, passphrase?.toCharArray(), HotAuth.NoAuth)
val hotUserWalletBuilder = hotUserWalletBuilderFactory.create(hotWalletId)
val userWallet = hotUserWalletBuilder.build()
saveUserWalletUseCase(userWallet)
params.callbacks.onWalletImported(userWallet.walletId)
}.onFailure {
Timber.e(it)
uiState.update {
it.copy(createWalletProgress = false)
}
}
}
}
}

View file

@ -35,7 +35,7 @@ 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.PrimaryButton
import com.tangem.core.ui.components.TangemTextFieldsDefault
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.core.ui.components.keyboardAsState
@ -91,12 +91,11 @@ internal fun AddExistingWalletImportContent(state: AddExistingWalletImportUM, mo
)
}
PrimaryButtonIconEnd(
PrimaryButton(
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,

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.domain.models.wallet.UserWalletId
import com.tangem.features.hotwallet.manualbackup.completed.ui.ManualBackupCompletedContent
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@ -27,10 +28,11 @@ internal class ManualBackupCompletedComponent @AssistedInject constructor(
}
interface ModelCallbacks {
fun onContinueClick()
fun onContinueClick(userWalletId: UserWalletId)
}
data class Params(
val userWalletId: UserWalletId,
val callbacks: ModelCallbacks,
)
}

View file

@ -20,7 +20,7 @@ internal class ManualBackupCompletedModel @Inject constructor(
internal val uiState: StateFlow<ManualBackupCompletedUM>
field = MutableStateFlow(
ManualBackupCompletedUM(
onContinueClick = params.callbacks::onContinueClick,
onContinueClick = { params.callbacks.onContinueClick(params.userWalletId) },
),
)
}

View file

@ -8,9 +8,8 @@ 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.core.ui.extensions.stringResourceSafe
import com.tangem.features.hotwallet.setaccesscode.ui.AccessCodeLayout
import com.tangem.core.res.R
import com.tangem.features.hotwallet.setaccesscode.ui.AccessCode
import com.tangem.domain.models.wallet.UserWalletId
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -28,32 +27,21 @@ internal class AccessCodeComponent @AssistedInject constructor(
DisableScreenshotsDisposableEffect()
AccessCodeLayout(
AccessCode(
modifier = modifier,
accessCode = state.accessCode,
onAccessCodeChange = state.onAccessCodeChange,
accessCodeLength = state.accessCodeLength,
reEnterAccessCodeState = params.isConfirmMode,
buttonText = stringResourceSafe(
if (params.isConfirmMode) {
R.string.common_confirm
} else {
R.string.common_continue
},
),
onButtonClick = state.onButtonClick,
buttonEnabled = state.buttonEnabled,
state = state,
)
}
interface ModelCallbacks {
fun onAccessCodeSet(accessCode: String)
fun onAccessCodeConfirmed()
fun onAccessCodeSet(userWalletId: UserWalletId, accessCode: String)
fun onAccessCodeConfirmed(userWalletId: UserWalletId)
}
data class Params(
val isConfirmMode: Boolean,
val accessCodeToConfirm: String? = null,
val userWalletId: UserWalletId,
val callbacks: ModelCallbacks,
)

View file

@ -1,14 +1,24 @@
package com.tangem.features.hotwallet.setaccesscode
import androidx.compose.runtime.Stable
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.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.setaccesscode.entity.AccessCodeUM
import com.tangem.hot.sdk.TangemHotSdk
import com.tangem.hot.sdk.model.HotAuth
import com.tangem.hot.sdk.model.UnlockHotWallet
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
@Stable
@ -16,6 +26,9 @@ import javax.inject.Inject
internal class AccessCodeModel @Inject constructor(
paramsContainer: ParamsContainer,
override val dispatchers: CoroutineDispatcherProvider,
private val getUserWalletUseCase: GetUserWalletUseCase,
private val saveWalletUseCase: SaveWalletUseCase,
private val tangemHotSdk: TangemHotSdk,
) : Model() {
private val params = paramsContainer.require<AccessCodeComponent.Params>()
@ -26,7 +39,9 @@ internal class AccessCodeModel @Inject constructor(
private fun getInitialState() = AccessCodeUM(
accessCode = "",
onAccessCodeChange = ::onAccessCodeChange,
isConfirmMode = params.isConfirmMode,
buttonEnabled = false,
buttonInProgress = false,
onButtonClick = ::onButtonClick,
)
@ -44,10 +59,40 @@ internal class AccessCodeModel @Inject constructor(
}
private fun onButtonClick() {
if (params.isConfirmMode) {
params.callbacks.onAccessCodeConfirmed()
if (!params.isConfirmMode) {
params.callbacks.onAccessCodeSet(params.userWalletId, uiState.value.accessCode)
} else {
params.callbacks.onAccessCodeSet(uiState.value.accessCode)
params.accessCodeToConfirm?.let {
setCode(params.userWalletId, it)
}
}
}
private fun setCode(userWalletId: UserWalletId, accessCode: String) {
modelScope.launch {
uiState.update {
it.copy(buttonInProgress = true)
}
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,
auth = HotAuth.Password(accessCode.toCharArray()),
)
saveWalletUseCase(userWallet.copy(hotWalletId = updatedHotWalletId))
params.callbacks.onAccessCodeConfirmed(params.userWalletId)
}
}.onFailure {
Timber.e(it)
uiState.update {
it.copy(buttonInProgress = false)
}
}
}
}
}

View file

@ -5,7 +5,9 @@ import com.tangem.features.hotwallet.setaccesscode.ACCESS_CODE_LENGTH
internal data class AccessCodeUM(
val accessCode: String,
val onAccessCodeChange: (String) -> Unit,
val isConfirmMode: Boolean,
val buttonEnabled: Boolean,
val buttonInProgress: Boolean,
val onButtonClick: () -> Unit,
) {
val accessCodeLength: Int = ACCESS_CODE_LENGTH

View file

@ -0,0 +1,136 @@
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.PrimaryButton
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.AccessCodeUM
@Suppress("LongParameterList", "LongMethod")
@Composable
internal fun AccessCode(state: AccessCodeUM, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
Column(
modifier = Modifier
.padding(top = 16.dp)
.weight(1f)
.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 (state.isConfirmMode) {
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 (state.isConfirmMode) {
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 = state.accessCode,
onValueChange = state.onAccessCodeChange,
)
}
}
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.imePadding(),
text = stringResourceSafe(
if (state.isConfirmMode) {
R.string.common_confirm
} else {
R.string.common_continue
},
),
onClick = state.onButtonClick,
enabled = state.buttonEnabled,
showProgress = state.buttonInProgress,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewSet() {
TangemThemePreview {
AccessCode(
state = AccessCodeUM(
accessCode = "",
onAccessCodeChange = {},
isConfirmMode = false,
buttonEnabled = false,
buttonInProgress = false,
onButtonClick = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun PreviewConfirm() {
TangemThemePreview {
AccessCode(
state = AccessCodeUM(
accessCode = "123456",
onAccessCodeChange = {},
isConfirmMode = true,
buttonEnabled = true,
buttonInProgress = false,
onButtonClick = {},
),
)
}
}

View file

@ -1,106 +0,0 @@
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
@Composable
internal fun AccessCodeEnter(
accessCode: String,
onAccessCodeChange: (String) -> Unit,
accessCodeLength: Int,
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,
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 = accessCodeLength,
isPasswordVisual = true,
value = accessCode,
onValueChange = onAccessCodeChange,
)
}
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview() {
TangemThemePreview {
AccessCodeEnter(
accessCode = "123456",
onAccessCodeChange = {},
accessCodeLength = 6,
reEnterAccessCodeState = false,
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview2() {
TangemThemePreview {
AccessCodeEnter(
accessCode = "123456",
onAccessCodeChange = {},
accessCodeLength = 6,
reEnterAccessCodeState = true,
)
}
}

View file

@ -1,51 +0,0 @@
package com.tangem.features.hotwallet.setaccesscode.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.PrimaryButton
@Suppress("LongParameterList")
@Composable
internal fun AccessCodeLayout(
accessCode: String,
onAccessCodeChange: (String) -> Unit,
accessCodeLength: Int,
reEnterAccessCodeState: Boolean,
buttonText: String,
onButtonClick: () -> Unit,
buttonEnabled: Boolean,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
AccessCodeEnter(
modifier = Modifier
.padding(top = 16.dp)
.weight(1f),
accessCode = accessCode,
onAccessCodeChange = onAccessCodeChange,
accessCodeLength = accessCodeLength,
reEnterAccessCodeState = reEnterAccessCodeState,
)
PrimaryButton(
modifier = Modifier
.fillMaxWidth()
.padding(start = 16.dp, end = 16.dp, bottom = 16.dp)
.imePadding(),
text = buttonText,
onClick = onButtonClick,
enabled = buttonEnabled,
)
}
}