From 9baa0a556bf4c08e5ad4743bde6ffc69c1ab65ba Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 23 Jul 2025 10:37:38 +0300 Subject: [PATCH] Updated on 2026-08-14 --- .../tap/di/domain/TransactionDomainModule.kt | 6 +- .../tap/domain/hot/HotWalletAccessor.kt | 105 +++++++++++--- .../tap/domain/hot/TangemHotWalletSigner.kt | 98 +++++++++++++ .../tap/domain/userWalletList/utils/Mapper.kt | 2 +- .../com/tangem/tap/routing/RootContent.kt | 4 + .../component/impl/DefaultRoutingComponent.kt | 19 ++- .../tangem/core/ui/components/FullScreen.kt | 30 +++- .../core/ui/components/fields/PinTextField.kt | 9 +- .../data/common/network/NetworkFactory.kt | 4 +- .../com/tangem/domain/models/MobileWallet.kt | 23 +++ .../tokens/GetCryptoCurrencyActionsUseCase.kt | 12 +- .../tokens/actions/BaseActionsFactory.kt | 2 +- .../tokens/actions/CommonActionsFactory.kt | 2 +- .../actions/OutdatedDataActionsFactory.kt | 2 +- .../actions/UnreachableActionsFactory.kt | 2 +- .../usecase/SendTransactionUseCase.kt | 4 +- .../wallets/builder/HotUserWalletBuilder.kt | 13 +- features/hot-wallet/api/build.gradle.kts | 3 + .../HotAccessCodeRequestComponent.kt | 9 ++ .../hotwallet/HotWalletPasswordRequester.kt | 18 +++ features/hot-wallet/impl/build.gradle.kts | 1 + .../DefaultHotAccessCodeRequestComponent.kt | 70 +++++++++ .../HotAccessCodeRequestModel.kt | 89 ++++++++++++ .../di/ComponentModuleBinds.kt | 32 ++++ .../entity/HotAccessCodeRequestUM.kt | 11 ++ .../proxy/HotWalletPasswordRequesterProxy.kt | 32 ++++ .../HotAccessCodeRequestFullScreenContent.kt | 137 ++++++++++++++++++ gradle/tangem_dependencies.toml | 2 +- 28 files changed, 691 insertions(+), 50 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt create mode 100644 features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt create mode 100644 features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt diff --git a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt index 73b6077ca8..c426af9672 100644 --- a/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt +++ b/app/src/main/java/com/tangem/tap/di/domain/TransactionDomainModule.kt @@ -11,7 +11,7 @@ import com.tangem.domain.transaction.FeeRepository import com.tangem.domain.transaction.TransactionRepository import com.tangem.domain.transaction.usecase.* import com.tangem.domain.walletmanager.WalletManagersFacade -import com.tangem.tap.domain.hot.TangemHotSigner +import com.tangem.tap.domain.hot.TangemHotWalletSigner import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -44,7 +44,7 @@ internal object TransactionDomainModule { transactionRepository: TransactionRepository, walletManagersFacade: WalletManagersFacade, singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - tangemHotSignerFactory: TangemHotSigner.Factory, + tangemHotWalletSignerFactory: TangemHotWalletSigner.Factory, ): SendTransactionUseCase { return SendTransactionUseCase( demoConfig = DemoConfig(), @@ -52,7 +52,7 @@ internal object TransactionDomainModule { transactionRepository = transactionRepository, walletManagersFacade = walletManagersFacade, singleNetworkStatusFetcher = singleNetworkStatusFetcher, - getHotSigner = tangemHotSignerFactory::create, + getHotWalletSigner = tangemHotWalletSignerFactory::create, ) } diff --git a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt b/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt index 5014f950b6..a65a2ecd53 100644 --- a/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt +++ b/app/src/main/java/com/tangem/tap/domain/hot/HotWalletAccessor.kt @@ -1,6 +1,9 @@ package com.tangem.tap.domain.hot +import com.tangem.common.core.TangemSdkError +import com.tangem.features.hotwallet.HotWalletPasswordRequester import com.tangem.hot.sdk.TangemHotSdk +import com.tangem.hot.sdk.exception.WrongPasswordException import com.tangem.hot.sdk.model.* import javax.inject.Inject @@ -12,33 +15,99 @@ class HotWalletAccessor @Inject constructor( suspend fun signHashes(hotWalletId: HotWalletId, dataToSign: List): List { val auth = when (hotWalletId.authType) { HotWalletId.AuthType.NoPassword -> HotAuth.NoAuth - HotWalletId.AuthType.Password -> { - hotWalletPasswordRequester.requestPassword(hotWalletId) - } + HotWalletId.AuthType.Password -> requestPassword(false) HotWalletId.AuthType.Biometry -> HotAuth.Biometry } - return runCatching { + return runCatchingSdkErrors(hotWalletId, auth) { tangemHotSdk.signHashes( unlockHotWallet = UnlockHotWallet( walletId = hotWalletId, - auth = auth, + auth = it, ), dataToSign = dataToSign, - ) - }.getOrElse { - if (hotWalletId.authType == HotWalletId.AuthType.Biometry) { - val passwordAuth = hotWalletPasswordRequester.requestPassword(hotWalletId) - tangemHotSdk.signHashes( - unlockHotWallet = UnlockHotWallet( - walletId = hotWalletId, - auth = passwordAuth, - ), - dataToSign = dataToSign, - ) - } else { - throw it + ).also { + hotWalletPasswordRequester.dismiss() } } } + + private suspend fun runCatchingSdkErrors( + hotWalletId: HotWalletId, + auth: HotAuth, + block: suspend (auth: HotAuth) -> T, + ): T { + return runCatchingWrongPassInternal( + originalAuth = auth, + auth = auth, + block = { blockAuth -> + block(blockAuth).also { + // TODO [REDACTED_TASK_KEY] if user has biometry enabled, we set it as the new auth method + if (blockAuth is HotAuth.Password /*&& has biometry enabled */) { + tangemHotSdk.changeAuth( + unlockHotWallet = UnlockHotWallet( + walletId = hotWalletId, + auth = blockAuth, + ), + auth = HotAuth.Biometry, + ) + } + } + }, + ) + } + + private suspend fun runCatchingWrongPassInternal( + originalAuth: HotAuth, + auth: HotAuth, + block: suspend (auth: HotAuth) -> T, + ): T = runCatching { + block(auth) + }.getOrElse { exception -> + if (auth is HotAuth.Biometry && exception.isBiometryError()) { + // fallback to password if biometry fails + val passAuth = requestPassword(true) + + return@getOrElse runCatchingWrongPassInternal( + originalAuth = originalAuth, + auth = passAuth, + block = block, + ) + } + + if (exception !is WrongPasswordException) { + throw exception + } + + // If the exception is a wrong password, we need to request the password again + + hotWalletPasswordRequester.wrongPassword() + val passResult = requestPassword(originalAuth is HotAuth.Biometry) + + runCatchingWrongPassInternal( + originalAuth = originalAuth, + auth = passResult, + block = block, + ) + } + + private suspend fun requestPassword(hasBiometry: Boolean): HotAuth { + return hotWalletPasswordRequester.requestPassword(hasBiometry).toAuth() ?: throw TangemSdkError.UserCancelled() + } + + private fun Throwable.isBiometryError(): Boolean { + return this is TangemSdkError.AuthenticationFailed || + this is TangemSdkError.AuthenticationCanceled || + this is TangemSdkError.AuthenticationLockout || + this is TangemSdkError.AuthenticationUnavailable || + this is TangemSdkError.AuthenticationAlreadyInProgress || + this is TangemSdkError.AuthenticationNotInitialized || + this is TangemSdkError.AuthenticationPermanentLockout + } + + private fun HotWalletPasswordRequester.Result.toAuth() = when (this) { + HotWalletPasswordRequester.Result.UseBiometry -> HotAuth.Biometry + HotWalletPasswordRequester.Result.Dismiss -> null + is HotWalletPasswordRequester.Result.EnteredPassword -> this.password + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt b/app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt new file mode 100644 index 0000000000..248d1c0369 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/hot/TangemHotWalletSigner.kt @@ -0,0 +1,98 @@ +package com.tangem.tap.domain.hot + +import com.tangem.blockchain.common.TransactionSigner +import com.tangem.blockchain.common.Wallet +import com.tangem.common.CompletionResult +import com.tangem.common.core.TangemSdkError +import com.tangem.common.map +import com.tangem.domain.wallets.models.UserWallet +import com.tangem.hot.sdk.model.DataToSign +import com.tangem.operations.sign.SignData +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import timber.log.Timber + +class TangemHotWalletSigner @AssistedInject constructor( + @Assisted private val userWallet: UserWallet.Hot, + private val hotWalletAccessor: HotWalletAccessor, +) : TransactionSigner { + + override suspend fun sign(hash: ByteArray, publicKey: Wallet.PublicKey): CompletionResult { + return sign(listOf(hash), publicKey).map { it.first() } + } + + override suspend fun sign( + hashes: List, + publicKey: Wallet.PublicKey, + ): CompletionResult> { + val wallet = userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(publicKey.seedKey) } + ?: return CompletionResult.Failure( + TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")), + ) + + val result = runCatching { + hotWalletAccessor.signHashes( + hotWalletId = userWallet.hotWalletId, + dataToSign = listOf( + DataToSign( + curve = wallet.curve, + hashes = hashes, + derivationPath = publicKey.derivationPath, + ), + ), + ) + }.getOrElse { + Timber.e(it) + return if (it is TangemSdkError) { + CompletionResult.Failure(it) + } else { + CompletionResult.Failure(TangemSdkError.ExceptionError(it)) + } + } + + return CompletionResult.Success(result.map { it.signatures }.flatten()) + } + + override suspend fun multiSign( + dataToSign: List, + publicKey: Wallet.PublicKey, + ): CompletionResult> { + val result = runCatching { + hotWalletAccessor.signHashes( + hotWalletId = userWallet.hotWalletId, + dataToSign = dataToSign.map { signData -> + val wallet = + userWallet.wallets.orEmpty().firstOrNull { it.publicKey.contentEquals(signData.publicKey) } + ?: return CompletionResult.Failure( + TangemSdkError.ExceptionError(IllegalStateException("wallet is locked")), + ) + + DataToSign( + curve = wallet.curve, + hashes = listOf(signData.hash), + derivationPath = signData.derivationPath, + ) + }, + ) + }.getOrElse { + Timber.e(it) + return if (it is TangemSdkError) { + CompletionResult.Failure(it) + } else { + CompletionResult.Failure(TangemSdkError.ExceptionError(it)) + } + } + + return CompletionResult.Success( + result.mapIndexed { index, data -> + dataToSign[index].publicKey to data.signatures.first() + }.toMap(), + ) + } + + @AssistedFactory + interface Factory { + fun create(@Assisted userWallet: UserWallet.Hot): TangemHotWalletSigner + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt index f8c9d84ea5..ec627a4e34 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/utils/Mapper.kt @@ -117,5 +117,5 @@ internal fun UserWallet.lock(): UserWallet = when (this) { ), ) } - is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]") + is UserWallet.Hot -> copy(wallets = null) } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/routing/RootContent.kt b/app/src/main/java/com/tangem/tap/routing/RootContent.kt index f312690af5..5c009849bc 100644 --- a/app/src/main/java/com/tangem/tap/routing/RootContent.kt +++ b/app/src/main/java/com/tangem/tap/routing/RootContent.kt @@ -34,6 +34,7 @@ import com.tangem.core.ui.res.TangemTheme import com.tangem.tap.routing.component.RoutingComponent import com.tangem.tap.routing.transitions.RoutingTransitionAnimationFactory +@Suppress("LongParameterList") @OptIn(ExperimentalDecomposeApi::class) @Composable internal fun RootContent( @@ -41,6 +42,7 @@ internal fun RootContent( backHandler: BackHandler, uiDependencies: UiDependencies, wcContent: @Composable (modifier: Modifier) -> Unit, + hotAccessCodeContent: @Composable (modifier: Modifier) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier, ) { @@ -74,6 +76,8 @@ internal fun RootContent( wcContent(Modifier.fillMaxSize()) + hotAccessCodeContent(Modifier.fillMaxSize()) + TangemSnackbarHost( modifier = Modifier .align(Alignment.BottomCenter) diff --git a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt index 909be69b49..86b7acd8d4 100644 --- a/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt +++ b/app/src/main/java/com/tangem/tap/routing/component/impl/DefaultRoutingComponent.kt @@ -10,12 +10,15 @@ import com.arkivanov.essenty.lifecycle.subscribe import com.google.android.material.snackbar.Snackbar import com.tangem.common.routing.AppRoute import com.tangem.core.decompose.context.AppComponentContext +import com.tangem.core.decompose.context.child import com.tangem.core.decompose.context.childByContext import com.tangem.core.decompose.navigation.getOrCreateTyped import com.tangem.core.ui.UiDependencies import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.message.SnackbarMessage +import com.tangem.features.hotwallet.HotAccessCodeRequestComponent +import com.tangem.features.hotwallet.accesscoderequest.proxy.HotWalletPasswordRequesterProxy import com.tangem.features.walletconnect.components.WcRoutingComponent import com.tangem.hot.sdk.TangemHotSdk import com.tangem.hot.sdk.android.create @@ -41,13 +44,20 @@ internal class DefaultRoutingComponent @AssistedInject constructor( private val wcRoutingComponentFactory: WcRoutingComponent.Factory, private val deeplinkFactory: DeepLinkFactory, private val tangemHotSDKProxy: TangemHotSDKProxy, + private val hotAccessCodeRequestComponentFactory: HotAccessCodeRequestComponent.Factory, + private val hotAccessCodeRequesterProxy: HotWalletPasswordRequesterProxy, ) : RoutingComponent, AppComponentContext by context, SnackbarHandler { private val wcRoutingComponent: WcRoutingComponent by lazy { wcRoutingComponentFactory - .create(childByContext(componentContext = this), params = Unit) + .create(child("wcRoutingComponent"), params = Unit) + } + + private val hotAccessCodeRequestComponent: HotAccessCodeRequestComponent by lazy { + hotAccessCodeRequestComponentFactory + .create(child("hotAccessCodeRequestComponent"), Unit) } private val stack: Value> = childStack( @@ -76,7 +86,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( } } - configureHotSdk() + configureProxies() } @Composable @@ -86,6 +96,7 @@ internal class DefaultRoutingComponent @AssistedInject constructor( stack = stack, uiDependencies = uiDependencies, wcContent = { wcRoutingComponent.Content(it) }, + hotAccessCodeContent = { hotAccessCodeRequestComponent.Content(it) }, backHandler = backHandler, onBack = router::pop, ) @@ -127,13 +138,15 @@ internal class DefaultRoutingComponent @AssistedInject constructor( initialStack } - private fun configureHotSdk() { + private fun configureProxies() { lifecycle.subscribe( onCreate = { tangemHotSDKProxy.sdkState.value = TangemHotSdk.create(activity) + hotAccessCodeRequesterProxy.componentRequester.value = hotAccessCodeRequestComponent }, onDestroy = { tangemHotSDKProxy.sdkState.value = null + hotAccessCodeRequesterProxy.componentRequester.value = null }, ) } diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/FullScreen.kt b/core/ui/src/main/java/com/tangem/core/ui/components/FullScreen.kt index 9f94bf859a..965c9c4cf0 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/FullScreen.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/FullScreen.kt @@ -3,6 +3,7 @@ package com.tangem.core.ui.components import android.annotation.SuppressLint import android.content.Context import android.graphics.PixelFormat +import android.view.KeyEvent import android.view.View import android.view.WindowManager import androidx.compose.runtime.* @@ -18,7 +19,12 @@ import androidx.savedstate.setViewTreeSavedStateRegistryOwner import java.util.UUID @Composable -fun FullScreen(notTouchable: Boolean = false, content: @Composable (() -> Unit) -> Unit) { +fun FullScreen( + notTouchable: Boolean = false, + focusable: Boolean = false, + onBackClick: () -> Unit = {}, + content: @Composable (() -> Unit) -> Unit, +) { val view = LocalView.current val parentComposition = rememberCompositionContext() val currentContent by rememberUpdatedState(content) @@ -27,7 +33,9 @@ fun FullScreen(notTouchable: Boolean = false, content: @Composable (() -> Unit) val fullScreenLayout = remember { FullScreenLayout( notTouchable = notTouchable, + focusable = focusable, composeView = view, + onBackClick = onBackClick, uniqueId = id, ).apply { setContent(parentComposition) { @@ -45,7 +53,9 @@ fun FullScreen(notTouchable: Boolean = false, content: @Composable (() -> Unit) @SuppressLint("ViewConstructor", "ClickableViewAccessibility") private class FullScreenLayout( private val notTouchable: Boolean, + private val focusable: Boolean, private val composeView: View, + private val onBackClick: () -> Unit, uniqueId: UUID, ) : AbstractComposeView(composeView.context) { @@ -97,7 +107,10 @@ private class FullScreenLayout( fun show() { if (viewShowing) dismiss() windowManager.addView(this, params) - params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + + if (focusable.not()) { + params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + } if (notTouchable) { params.flags = params.flags or WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE @@ -107,6 +120,19 @@ private class FullScreenLayout( viewShowing = true } + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + if (focusable.not()) { + return super.dispatchKeyEvent(event) + } + + if (event.keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { + onBackClick() + return true + } + + return super.dispatchKeyEvent(event) + } + fun dismiss() { if (!viewShowing) return disposeComposition() diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt index a06106b354..fa00fb9959 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/PinTextField.kt @@ -38,6 +38,7 @@ fun PinTextField( isPasswordVisual: Boolean, onValueChange: (String) -> Unit, modifier: Modifier = Modifier, + wrongCode: Boolean = false, ) { val focusRequester = remember { FocusRequester() } val textFieldValue = remember(value) { @@ -71,6 +72,7 @@ fun PinTextField( CellDecoration( length = length, isPasswordVisual = isPasswordVisual, + wrongCode = wrongCode, value = value, ) }, @@ -86,6 +88,7 @@ fun PinTextField( @Composable private fun CellDecoration( length: Int, + wrongCode: Boolean, value: String, modifier: Modifier = Modifier, isPasswordVisual: Boolean = false, @@ -128,7 +131,11 @@ private fun CellDecoration( modifier = Modifier.sizeIn(minWidth = width.size.width.dp + 8.dp, minHeight = 48.dp), text = text, style = TangemTheme.typography.h3, - color = TangemTheme.colors.text.primary1, + color = if (wrongCode) { + TangemTheme.colors.text.warning + } else { + TangemTheme.colors.text.primary1 + }, textAlign = TextAlign.Center, lineHeight = 48.sp, ) diff --git a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt index c0b7e6003b..94ad70a15a 100644 --- a/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt +++ b/data/common/src/main/kotlin/com/tangem/data/common/network/NetworkFactory.kt @@ -30,7 +30,7 @@ class NetworkFactory @Inject constructor( * * @param blockchain blockchain * @param extraDerivationPath extra derivation path - * @param userWallet user wallet + * @param userWallet user wallet */ fun create(blockchain: Blockchain, extraDerivationPath: String?, userWallet: UserWallet): Network? { return create( @@ -52,7 +52,7 @@ class NetworkFactory @Inject constructor( * * @param networkId network id * @param derivationPath derivation path - * @param userWallet user wallet + * @param userWallet user wallet */ fun create(networkId: Network.ID, derivationPath: Network.DerivationPath, userWallet: UserWallet): Network? { val blockchain = networkId.toBlockchain() diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/MobileWallet.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/MobileWallet.kt index 0417cea67b..4ceab71fb6 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/MobileWallet.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/MobileWallet.kt @@ -28,4 +28,27 @@ data class MobileWallet( chainCode = it, ) } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is MobileWallet) return false + + if (!publicKey.contentEquals(other.publicKey)) return false + if (chainCode != null) { + if (other.chainCode == null || !chainCode.contentEquals(other.chainCode)) return false + } else if (other.chainCode != null) return false + + if (curve != other.curve) return false + if (derivedKeys != other.derivedKeys) return false + + return true + } + + override fun hashCode(): Int { + var result = publicKey.contentHashCode() + result = 31 * result + (chainCode?.contentHashCode() ?: 0) + result = 31 * result + curve.hashCode() + result = 31 * result + derivedKeys.hashCode() + return result + } } \ No newline at end of file diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt index 51e64f34bd..e1b0f65499 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/GetCryptoCurrencyActionsUseCase.kt @@ -53,18 +53,8 @@ class GetCryptoCurrencyActionsUseCase( rampStateManager = rampManager, ) - operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { - return when (userWallet) { - is UserWallet.Cold -> coldFlow(userWallet, cryptoCurrencyStatus) - is UserWallet.Hot -> TODO("[REDACTED_TASK_KEY]") - } - } - @OptIn(ExperimentalCoroutinesApi::class) - private fun coldFlow( - userWallet: UserWallet.Cold, - cryptoCurrencyStatus: CryptoCurrencyStatus, - ): Flow { + operator fun invoke(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Flow { return when { cryptoCurrencyStatus.value is CryptoCurrencyStatus.MissedDerivation -> { flowOf(value = MissedDerivationsActionsFactory.create()) diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt index 81193b625e..6feda808cf 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/BaseActionsFactory.kt @@ -58,7 +58,7 @@ internal open class BaseActionsFactory( * @param currency the cryptocurrency to check */ protected suspend fun getOnrampUnavailabilityReason( - userWallet: UserWallet.Cold, + userWallet: UserWallet, currency: CryptoCurrency, ): ScenarioUnavailabilityReason { return rampStateManager.availableForBuy( diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt index 2f97aacfe1..41890a7320 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/CommonActionsFactory.kt @@ -35,7 +35,7 @@ internal class CommonActionsFactory( * @param shouldShowSwapStories a flag indicating whether to show swap stories */ suspend fun create( - userWallet: UserWallet.Cold, + userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, stakingAvailability: StakingAvailability, shouldShowSwapStories: Boolean, diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt index ff4d60c1c5..f38522a4f7 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/OutdatedDataActionsFactory.kt @@ -33,7 +33,7 @@ internal class OutdatedDataActionsFactory( * @param stakingAvailability the staking availability for the cryptocurrency */ suspend fun create( - userWallet: UserWallet.Cold, + userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus, stakingAvailability: StakingAvailability, ): Set = coroutineScope { diff --git a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt index 1e42d795e1..ded293f444 100644 --- a/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt +++ b/domain/tokens/src/main/kotlin/com/tangem/domain/tokens/actions/UnreachableActionsFactory.kt @@ -22,7 +22,7 @@ internal class UnreachableActionsFactory( rampStateManager: RampStateManager, ) : BaseActionsFactory(walletManagersFacade, rampStateManager) { - suspend fun create(userWallet: UserWallet.Cold, cryptoCurrencyStatus: CryptoCurrencyStatus): Set = + suspend fun create(userWallet: UserWallet, cryptoCurrencyStatus: CryptoCurrencyStatus): Set = coroutineScope { val isAddressAvailable = isAddressAvailable(cryptoCurrencyStatus.value.networkAddress) diff --git a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt index 4611705bbe..c7eecef034 100644 --- a/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt +++ b/domain/transaction/src/main/java/com/tangem/domain/transaction/usecase/SendTransactionUseCase.kt @@ -32,7 +32,7 @@ class SendTransactionUseCase( private val transactionRepository: TransactionRepository, private val walletManagersFacade: WalletManagersFacade, private val singleNetworkStatusFetcher: SingleNetworkStatusFetcher, - private val getHotSigner: (UserWallet.Hot) -> TransactionSigner, + private val getHotWalletSigner: (UserWallet.Hot) -> TransactionSigner, ) { suspend operator fun invoke( txsData: List, @@ -53,7 +53,7 @@ class SendTransactionUseCase( coldSigner } is UserWallet.Hot -> { - getHotSigner(userWallet) + getHotWalletSigner(userWallet) } } diff --git a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt index 38fb282cda..ba55e47d78 100644 --- a/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt +++ b/domain/wallets/src/main/java/com/tangem/domain/wallets/builder/HotUserWalletBuilder.kt @@ -1,5 +1,6 @@ package com.tangem.domain.wallets.builder +import com.tangem.blockchain.blockchains.cardano.CardanoUtils import com.tangem.blockchain.common.Blockchain import com.tangem.blockchain.common.derivation.DerivationStyle import com.tangem.domain.models.MobileWallet @@ -24,11 +25,19 @@ class HotUserWalletBuilder @AssistedInject constructor( ) { suspend fun build(): UserWallet.Hot = withContext(dispatcherProvider.default) { - val allNetworks = Blockchain.entries // TODO use HotDerivationsRepository to get supported networks + val allNetworks = Blockchain.entries val curves = allNetworks.map { it.getSupportedCurves() }.flatten().toSet() val requests = curves.sortedBy { it.ordinal }.map { curve -> val derivationPaths = allNetworks.filter { curve in it.getSupportedCurves() } - .mapNotNull { it.derivationPath(DerivationStyle.V3) } + .mapNotNull { + val derivationPath = it.derivationPath(DerivationStyle.V3) ?: return@mapNotNull null + if (it == Blockchain.Cardano) { + val extendedDerivationPath = CardanoUtils.extendedDerivationPath(derivationPath) + listOf(derivationPath, extendedDerivationPath) + } else { + listOf(derivationPath) + } + }.flatten() DeriveWalletRequest.Request( curve = curve, diff --git a/features/hot-wallet/api/build.gradle.kts b/features/hot-wallet/api/build.gradle.kts index 0b7e98f231..310329cc67 100644 --- a/features/hot-wallet/api/build.gradle.kts +++ b/features/hot-wallet/api/build.gradle.kts @@ -23,4 +23,7 @@ dependencies { /* Compose */ implementation(deps.compose.runtime) + + /* Tangem libs */ + implementation(tangemDeps.hot.core) } \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt new file mode 100644 index 0000000000..6e6c9c7c8b --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotAccessCodeRequestComponent.kt @@ -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 +} \ No newline at end of file diff --git a/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt new file mode 100644 index 0000000000..eac354ece4 --- /dev/null +++ b/features/hot-wallet/api/src/main/kotlin/com/tangem/features/hotwallet/HotWalletPasswordRequester.kt @@ -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() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/build.gradle.kts b/features/hot-wallet/impl/build.gradle.kts index e9081d8611..09ab226d38 100644 --- a/features/hot-wallet/impl/build.gradle.kts +++ b/features/hot-wallet/impl/build.gradle.kts @@ -31,6 +31,7 @@ dependencies { implementation(projects.domain.models) implementation(projects.domain.wallets) implementation(projects.domain.wallets.models) + implementation(projects.domain.models) /** Common */ implementation(projects.common.ui) diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt new file mode 100644 index 0000000000..beaba4f29a --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/DefaultHotAccessCodeRequestComponent.kt @@ -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 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt new file mode 100644 index 0000000000..28fb18f009 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/HotAccessCodeRequestModel.kt @@ -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(null) + + val uiState: StateFlow + 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 + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt new file mode 100644 index 0000000000..b1fb298d59 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/di/ComponentModuleBinds.kt @@ -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 +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt new file mode 100644 index 0000000000..82b7e51ec8 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/entity/HotAccessCodeRequestUM.kt @@ -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 = {}, +) \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt new file mode 100644 index 0000000000..29a16c9c2f --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/proxy/HotWalletPasswordRequesterProxy.kt @@ -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(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 call(block: suspend HotWalletPasswordRequester.() -> T): T { + return withTimeout(timeMillis = 1000) { + componentRequester.filterNotNull().first() + }.block() + } +} \ No newline at end of file diff --git a/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt new file mode 100644 index 0000000000..bc64b660c4 --- /dev/null +++ b/features/hot-wallet/impl/src/main/kotlin/com/tangem/features/hotwallet/accesscoderequest/ui/HotAccessCodeRequestFullScreenContent.kt @@ -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") + } + } + } +} \ No newline at end of file diff --git a/gradle/tangem_dependencies.toml b/gradle/tangem_dependencies.toml index 80b280a86a..a128712775 100644 --- a/gradle/tangem_dependencies.toml +++ b/gradle/tangem_dependencies.toml @@ -11,7 +11,7 @@ tangemCardSdk = "develop-501" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds ^ tangemVico = "2.0.0-alpha.25-tangem12" #tangemVico = "0.0.1" # Keep it! - used for local builds ^ -tangemHotSdk = "develop-438" +tangemHotSdk = "develop-441" #tangemHotSdk = "0.0.1" # Keep it! - used for local builds ^