Updated on 2026-08-14

This commit is contained in:
Tangem 2025-01-16 11:04:05 +03:00
commit c96350c53a
30 changed files with 604 additions and 37 deletions

View file

@ -1,3 +1,5 @@
import java.util.Properties
plugins {
alias(deps.plugins.android.application)
alias(deps.plugins.kotlin.android)
@ -25,6 +27,23 @@ android {
androidResources {
generateLocaleConfig = true
}
signingConfigs {
getByName("debug") {
val keystorePath = "src/main/assets/tangem-app-config/android/keystore/debug_keystore"
val propertiesPath = "src/main/assets/tangem-app-config/android/keystore/debug_keystore.properties"
val keystoreProperties = Properties()
file(propertiesPath)
.inputStream()
.use { keystoreProperties.load(it) }
storeFile = file(keystorePath)
storePassword = keystoreProperties["store_password"] as String
keyAlias = keystoreProperties["key_alias"] as String
keyPassword = keystoreProperties["key_password"] as String
}
}
}
configurations.all {

@ -1 +1 @@
Subproject commit e91a2992857afa5cefce05c2750daf175b783603
Subproject commit c36c77faf2101b30f472b2d5ba00dc0b48af9baa

View file

@ -0,0 +1,42 @@
package com.tangem.tap.data
import android.content.Context
import com.tangem.common.services.secure.SecureStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.sdk.storage.AndroidSecureStorage
import com.tangem.sdk.storage.createEncryptedSharedPreferences
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.withContext
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DefaultVisaOTPStorage @Inject constructor(
@ApplicationContext applicationContext: Context,
private val dispatcherProvider: CoroutineDispatcherProvider,
) : VisaOTPStorage {
private val secureStorage = AndroidSecureStorage(
preferences = SecureStorage.createEncryptedSharedPreferences(
context = applicationContext,
storageName = "visa_otp_storage",
),
)
override suspend fun saveOTP(cardId: String, otp: ByteArray) = withContext(dispatcherProvider.io) {
secureStorage.store(otp, VISA_OTP_KEY_PREFIX + cardId)
}
override suspend fun getOTP(cardId: String): ByteArray? = withContext(dispatcherProvider.io) {
secureStorage.get(VISA_OTP_KEY_PREFIX + cardId)
}
override suspend fun removeOTP(cardId: String) = withContext(dispatcherProvider.io) {
secureStorage.delete(VISA_OTP_KEY_PREFIX + cardId)
}
private companion object {
const val VISA_OTP_KEY_PREFIX = "visa_otp_"
}
}

View file

@ -6,6 +6,7 @@ import com.tangem.domain.card.repository.CardSdkConfigRepository
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.tap.domain.sdk.impl.DefaultTangemSdkManager
import com.tangem.tap.domain.sdk.impl.MockTangemSdkManager
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.visa.VisaCardScanHandler
import dagger.Module
import dagger.Provides
@ -24,6 +25,7 @@ internal class TangemSdkManagerModule {
@ApplicationContext context: Context,
cardSdkConfigRepository: CardSdkConfigRepository,
visaCardScanHandler: VisaCardScanHandler,
visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
): TangemSdkManager {
return if (BuildConfig.MOCK_DATA_SOURCE) {
MockTangemSdkManager(resources = context.resources)
@ -32,6 +34,7 @@ internal class TangemSdkManagerModule {
cardSdkConfigRepository = cardSdkConfigRepository,
resources = context.resources,
visaCardScanHandler = visaCardScanHandler,
visaCardActivationTaskFactory = visaCardActivationTaskFactory,
)
}
}

View file

@ -1,7 +1,9 @@
package com.tangem.tap.di.data
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.tap.data.DefaultVisaAuthTokenStorage
import com.tangem.tap.data.DefaultVisaOTPStorage
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
@ -15,4 +17,8 @@ internal interface VisaStorageModule {
@Binds
@Singleton
fun bindVisaStorage(impl: DefaultVisaAuthTokenStorage): VisaAuthTokenStorage
@Binds
@Singleton
fun bindVisaOTPStorage(impl: DefaultVisaOTPStorage): VisaOTPStorage
}

View file

@ -24,6 +24,9 @@ import com.tangem.domain.common.util.cardTypesResolver
import com.tangem.domain.common.util.derivationStyleProvider
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.ScanTask
import com.tangem.operations.derivation.DerivationTaskResponse
@ -40,6 +43,7 @@ import com.tangem.tap.domain.tasks.product.CreateProductWalletTask
import com.tangem.tap.domain.tasks.product.ResetBackupCardTask
import com.tangem.tap.domain.tasks.product.ResetToFactorySettingsTask
import com.tangem.tap.domain.tasks.product.ScanProductTask
import com.tangem.tap.domain.tasks.visa.VisaCardActivationTask
import com.tangem.tap.domain.twins.CreateFirstTwinWalletTask
import com.tangem.tap.domain.twins.CreateSecondTwinWalletTask
import com.tangem.tap.domain.twins.FinalizeTwinTask
@ -55,6 +59,7 @@ internal class DefaultTangemSdkManager(
private val cardSdkConfigRepository: CardSdkConfigRepository,
private val resources: Resources,
private val visaCardScanHandler: VisaCardScanHandler,
private val visaCardActivationTaskFactory: VisaCardActivationTask.Factory,
) : TangemSdkManager {
private val awaitInitializationMutex = Mutex()
@ -466,6 +471,28 @@ internal class DefaultTangemSdkManager(
// endregion
// region Visa-specific
override suspend fun activateVisaCard(
accessCode: String,
challengeToSign: VisaAuthChallenge.Card?,
activationInput: VisaActivationInput,
): CompletionResult<VisaCardActivationResponse> {
return coroutineScope {
runTaskAsyncReturnOnMain(
runnable = visaCardActivationTaskFactory.create(
accessCode = accessCode,
challengeToSign = challengeToSign,
activationInput = activationInput,
coroutineScope = this,
),
initialMessage = Message(resources.getStringSafe(R.string.initial_message_tap_header)),
)
}
}
// endregion
companion object {
private const val MAX_INITIALIZE_ATTEMPTS = 10
}

View file

@ -17,6 +17,9 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
@ -194,4 +197,14 @@ class MockTangemSdkManager(
}
// endregion
// region Visa-specific
override suspend fun activateVisaCard(
accessCode: String,
challengeToSign: VisaAuthChallenge.Card?,
activationInput: VisaActivationInput,
): CompletionResult<VisaCardActivationResponse> {
error("Not implemented")
}
}

View file

@ -0,0 +1,268 @@
package com.tangem.tap.domain.tasks.visa
import com.reown.util.hexToBytes
import com.tangem.common.CompletionResult
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.CompletionCallback
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.toHexString
import com.tangem.common.map
import com.tangem.datasource.local.visa.VisaAuthTokenStorage
import com.tangem.datasource.local.visa.VisaOTPStorage
import com.tangem.domain.common.visa.VisaUtilities
import com.tangem.domain.visa.model.*
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.domain.visa.repository.VisaAuthRepository
import com.tangem.operations.GenerateOTPCommand
import com.tangem.operations.attestation.AttestCardKeyCommand
import com.tangem.operations.pins.SetUserCodeCommand
import com.tangem.operations.sign.SignHashCommand
import com.tangem.operations.sign.SignHashResponse
import com.tangem.operations.wallet.CreateWalletTask
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.*
import kotlin.coroutines.resume
import kotlin.jvm.Throws
@Suppress("LongParameterList")
class VisaCardActivationTask @AssistedInject constructor(
@Assisted private val accessCode: String,
@Assisted private val challengeToSign: VisaAuthChallenge.Card?,
@Assisted private val activationInput: VisaActivationInput,
@Assisted private val coroutineScope: CoroutineScope,
private val otpStorage: VisaOTPStorage,
private val visaAuthTokenStorage: VisaAuthTokenStorage,
private val visaAuthRepository: VisaAuthRepository,
private val visaActivationRepository: VisaActivationRepository,
) : CardSessionRunnable<VisaCardActivationResponse> {
override fun run(session: CardSession, callback: CompletionCallback<VisaCardActivationResponse>) {
coroutineScope.launch {
callback(runSuspend(session))
}
}
private suspend fun runSuspend(session: CardSession): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
if (card.cardId != activationInput.cardId) {
return CompletionResult.Failure(TangemSdkError.Underlying(VisaActivationError.WrongCard.message))
}
return if (challengeToSign != null) {
signAuthorizationChallenge(session, challengeToSign)
} else {
val activationOrder = runCatching { visaActivationRepository.getActivationOrderToSign() }
.getOrElse {
return CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
}
signOrder(session, activationOrder)
}
}
private suspend fun signAuthorizationChallenge(
session: CardSession,
challengeToSign: VisaAuthChallenge.Card,
): CompletionResult<VisaCardActivationResponse> {
val attestationCommand = AttestCardKeyCommand(challenge = challengeToSign.challenge.hexToBytes())
val result = suspendCancellableCoroutine { continuation ->
attestationCommand.run(session = session) { attestationResponse ->
continuation.resume(attestationResponse)
}
}
return when (result) {
is CompletionResult.Success -> {
processSignedAuthorizationChallenge(
session = session,
signedChallenge = challengeToSign.toSignedChallenge(
signedChallenge = result.data.cardSignature.toHexString(),
salt = result.data.salt.toHexString(),
),
)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
private suspend fun processSignedAuthorizationChallenge(
session: CardSession,
signedChallenge: VisaAuthSignedChallenge,
): CompletionResult<VisaCardActivationResponse> {
return coroutineScope {
val activationOrderDeferred = async { getActivationOrderToSign(signedChallenge) }
val otpTaskDeferred = async { createWallet(session) }
val order = runCatching { activationOrderDeferred.await() }
.getOrElse {
otpTaskDeferred.cancel()
return@coroutineScope CompletionResult.Failure(TangemSdkError.Underlying(it.message ?: ""))
}
otpTaskDeferred.await()
signOrder(session, order)
}
}
@Throws(TangemSdkError::class)
private suspend fun getActivationOrderToSign(signedChallenge: VisaAuthSignedChallenge): ActivationOrder {
val tokens = runCatching {
visaAuthRepository.getAccessTokens(signedChallenge)
}.getOrElse {
throw TangemSdkError.Underlying(
"Underlying network error: ${it.message ?: ""}",
)
}
visaAuthTokenStorage.store(tokens)
return visaActivationRepository.getActivationOrderToSign()
}
private suspend fun createWallet(session: CardSession): CompletionResult<Unit> {
coroutineScope { ensureActive() }
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
return if (card.wallets.any { it.curve == VisaUtilities.mandatoryCurve }) {
createOTP(session)
} else {
val createWalletTask = CreateWalletTask(VisaUtilities.mandatoryCurve)
val result = suspendCancellableCoroutine { continuation ->
createWalletTask.run(session) { createWalletResult ->
continuation.resume(createWalletResult)
}
}
when (result) {
is CompletionResult.Success -> {
createOTP(session)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
}
private suspend fun createOTP(session: CardSession): CompletionResult<Unit> {
coroutineScope { ensureActive() }
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val otp = otpStorage.getOTP(card.cardId)
return if (otp != null) {
CompletionResult.Success(Unit)
} else {
val otpCommand = GenerateOTPCommand()
val result = suspendCancellableCoroutine { continuation ->
otpCommand.run(session) { otpResult ->
continuation.resume(otpResult)
}
}
when (result) {
is CompletionResult.Success -> {
otpStorage.saveOTP(card.cardId, result.data.rootOTP)
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
}
private suspend fun signOrder(
session: CardSession,
order: ActivationOrder,
): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val wallet =
card.wallets.firstOrNull() ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val task = SignHashCommand(order.hash.hexToBytes(), wallet.publicKey)
val result = suspendCancellableCoroutine { continuation ->
task.run(session) { signResult ->
continuation.resume(signResult)
}
}
return when (result) {
is CompletionResult.Success -> {
handleSignedOrder(
session = session,
activationOrder = order,
response = result.data,
)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
private suspend fun handleSignedOrder(
session: CardSession,
activationOrder: ActivationOrder,
response: SignHashResponse,
): CompletionResult<VisaCardActivationResponse> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
val signedOrder = SignedActivationOrder(
activationOrder = activationOrder,
signature = response.signature.toHexString(),
)
val otp = otpStorage.getOTP(card.cardId) ?: return CompletionResult.Failure(
TangemSdkError.Underlying(VisaActivationError.MissingRootOTP.message),
)
val activationResponse = VisaCardActivationResponse(
signedActivationOrder = signedOrder,
rootOTP = VisaRootOTP(otp.toHexString()),
)
return setupAccessCode(session).map { activationResponse }
}
private suspend fun setupAccessCode(session: CardSession): CompletionResult<Unit> {
val card = session.environment.card ?: return CompletionResult.Failure(TangemSdkError.MissingPreflightRead())
if (card.isAccessCodeSet) {
return CompletionResult.Success(Unit)
}
val task = SetUserCodeCommand.changeAccessCode(accessCode)
val result = suspendCancellableCoroutine { continuation ->
task.run(session) { setAccessCodeResult ->
continuation.resume(setAccessCodeResult)
}
}
return when (result) {
is CompletionResult.Success -> {
CompletionResult.Success(Unit)
}
is CompletionResult.Failure -> {
CompletionResult.Failure(result.error)
}
}
}
@AssistedFactory
interface Factory {
fun create(
accessCode: String,
challengeToSign: VisaAuthChallenge.Card?,
activationInput: VisaActivationInput,
coroutineScope: CoroutineScope,
): VisaCardActivationTask
}
}

View file

@ -61,7 +61,7 @@ internal object UserWalletsListManagerModule {
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaCardActivationStatus.serializer)
.add(VisaCardActivationStatus.jsonAdapter)
.add(KotlinJsonAdapterFactory())
.build()

View file

@ -10,6 +10,7 @@ import com.tangem.datasource.api.common.adapter.LocalDateAdapter
import com.tangem.datasource.api.common.adapter.addStakeKitEnumFallbackAdapters
import com.tangem.datasource.local.config.providers.models.ProviderModel
import com.tangem.domain.models.scan.serialization.*
import com.tangem.domain.visa.model.VisaCardActivationStatus
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@ -34,6 +35,7 @@ class MoshiModule {
.add(BigDecimalAdapter())
.add(LocalDateAdapter())
.add(DateTimeAdapter())
.add(VisaCardActivationStatus.jsonAdapter)
.add(KotlinJsonAdapterFactory())
.addStakeKitEnumFallbackAdapters()
.build()
@ -42,8 +44,26 @@ class MoshiModule {
@Provides
@Singleton
@SdkMoshi
fun provideSdkMoshi(sdkMoshiJsonConverter: MoshiJsonConverter): Moshi {
return sdkMoshiJsonConverter.moshi
fun provideSdkMoshi(): Moshi {
val adapters = MoshiJsonConverter.getTangemSdkAdapters() +
listOf(
BigDecimalAdapter(),
WalletDerivedKeysMapAdapter(),
ScanResponseDerivedKeysMapAdapter(),
ByteArrayKeyAdapter(),
ExtendedPublicKeysMapAdapter(),
CardBackupStatusAdapter(),
DerivationPathAdapterWithMigration(),
)
val typedAdapters = MoshiJsonConverter.getTangemSdkTypedAdapters()
return Moshi.Builder().apply {
add(VisaCardActivationStatus.jsonAdapter)
adapters.forEach { this.add(it) }
typedAdapters.forEach { add(it.key, it.value) }
add(KotlinJsonAdapterFactory())
}.build()
}
@Provides

View file

@ -0,0 +1,12 @@
package com.tangem.datasource.local.visa
interface VisaOTPStorage {
suspend fun saveOTP(cardId: String, otp: ByteArray)
suspend fun getOTP(cardId: String): ByteArray?
suspend fun removeOTP(cardId: String)
}
suspend fun VisaOTPStorage.hasSavedOTP(cardId: String): Boolean = getOTP(cardId) != null

View file

@ -0,0 +1,38 @@
package com.tangem.core.ui.components.fields.contextmenu
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.NonSkippableComposable
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.platform.LocalTextToolbar
import androidx.compose.ui.platform.TextToolbar
import androidx.compose.ui.platform.TextToolbarStatus
/**
* Composable component that used to disable context menu
*
* @param content content
*/
@Composable
@NonSkippableComposable
fun DisableContextMenu(content: @Composable () -> Unit) {
CompositionLocalProvider(
value = LocalTextToolbar provides EmptyTextToolbar,
content = content,
)
}
private object EmptyTextToolbar : TextToolbar {
override val status: TextToolbarStatus = TextToolbarStatus.Hidden
override fun hide() = Unit
override fun showMenu(
rect: Rect,
onCopyRequested: (() -> Unit)?,
onPasteRequested: (() -> Unit)?,
onCutRequested: (() -> Unit)?,
onSelectAllRequested: (() -> Unit)?,
) = Unit
}

View file

@ -2,6 +2,7 @@ package com.tangem.data.visa
import com.tangem.data.visa.converter.VisaActivationStatusConverter
import com.tangem.datasource.api.visa.TangemVisaApi
import com.tangem.domain.visa.model.ActivationOrder
import com.tangem.domain.visa.model.VisaActivationRemoteState
import com.tangem.domain.visa.repository.VisaActivationRepository
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
@ -20,4 +21,8 @@ internal class DefaultVisaActivationRepository @Inject constructor(
visaActivationStatusConverter.convert(visaApi.getRemoteActivationStatus())
// TODO implement refreshing access token if it's expired
}
override suspend fun getActivationOrderToSign(): ActivationOrder {
return ActivationOrder("TODO implement")
}
}

View file

@ -22,7 +22,6 @@ import com.tangem.datasource.asset.loader.AssetLoader
import com.tangem.datasource.local.userwallet.UserWalletsStore
import com.tangem.datasource.local.walletmanager.WalletManagersStore
import com.tangem.domain.common.util.hasDerivation
import com.tangem.domain.demo.BuildConfig
import com.tangem.domain.demo.DemoConfig
import com.tangem.domain.tokens.model.CryptoCurrency
import com.tangem.domain.tokens.model.Network
@ -370,7 +369,7 @@ class DefaultWalletManagersFacade(
blockchain = blockchain,
derivationPath = derivationPath,
)
if (BuildConfig.DEBUG || walletManager == null) {
if (walletManager == null) {
walletManager = walletManagerFactory.createWalletManager(
scanResponse = userWallet.scanResponse,
blockchain = blockchain,

View file

@ -23,7 +23,7 @@ internal object ScanResponseAsStringSerializer : KSerializer<ScanResponse> {
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(VisaCardActivationStatus.serializer)
.add(VisaCardActivationStatus.jsonAdapter)
.add(KotlinJsonAdapterFactory())
.build()

View file

@ -0,0 +1,5 @@
package com.tangem.domain.visa.model
data class ActivationOrder(
val hash: String,
)

View file

@ -0,0 +1,6 @@
package com.tangem.domain.visa.model
data class SignedActivationOrder(
val activationOrder: ActivationOrder,
val signature: String,
)

View file

@ -3,4 +3,6 @@ package com.tangem.domain.visa.model
enum class VisaActivationError(val message: String) {
BlockedForActivation("Card is blocked for activation"),
InvalidActivationState("Invalid activation state"),
WrongCard("Wrong card tapped"),
MissingRootOTP("Missing root OTP"),
}

View file

@ -0,0 +1,6 @@
package com.tangem.domain.visa.model
data class VisaCardActivationResponse(
val signedActivationOrder: SignedActivationOrder,
val rootOTP: VisaRootOTP,
)

View file

@ -28,7 +28,7 @@ sealed class VisaCardActivationStatus {
data object Blocked : VisaCardActivationStatus()
companion object {
val serializer: PolymorphicJsonAdapterFactory<VisaCardActivationStatus>
val jsonAdapter: PolymorphicJsonAdapterFactory<VisaCardActivationStatus>
get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type")
.withSubtype(Activated::class.java, "Activated")
.withSubtype(ActivationStarted::class.java, "ActivationStarted")

View file

@ -0,0 +1,4 @@
package com.tangem.domain.visa.model
@JvmInline
value class VisaRootOTP(val value: String)

View file

@ -1,8 +1,11 @@
package com.tangem.domain.visa.repository
import com.tangem.domain.visa.model.ActivationOrder
import com.tangem.domain.visa.model.VisaActivationRemoteState
interface VisaActivationRepository {
suspend fun getActivationRemoteState(): VisaActivationRemoteState
suspend fun getActivationOrderToSign(): ActivationOrder
}

View file

@ -1,11 +1,11 @@
package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui
import androidx.compose.animation.*
import com.tangem.core.ui.extensions.stringResourceSafe
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.OutlinedTextField
import androidx.compose.material3.Text
@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
@ -24,6 +25,7 @@ import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.*
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
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.onboarding.v2.impl.R
@ -88,6 +90,10 @@ internal fun MultiWalletSeedPhraseImport(state: MultiWalletSeedPhraseUM.Import,
label = stringResourceSafe(id = R.string.common_passphrase),
placeholder = stringResourceSafe(id = R.string.send_optional_field),
onIconClick = state.onPassphraseInfoClick,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
}
@ -142,6 +148,10 @@ private fun PhraseBlock(state: MultiWalletSeedPhraseUM.Import, modifier: Modifie
singleLine = false,
colors = TangemTextFieldsDefault.defaultTextFieldColors,
visualTransformation = invalidWordsColorTransformation,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
Box(

View file

@ -2,19 +2,27 @@ package com.tangem.features.onboarding.v2.multiwallet.impl.child.seedphrase.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.R
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButtonUM
import com.tangem.core.ui.components.OutlineTextField
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
@ -96,17 +104,30 @@ internal fun MultiWalletSeedPhraseWordsCheck(
@Composable
private fun Fields(state: MultiWalletSeedPhraseUM.GeneratedWordsCheck, modifier: Modifier = Modifier) {
val focusManager = LocalFocusManager.current
Column(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
state.wordFields.forEach { field ->
OutlineTextField(
value = field.word,
onValueChange = field.onChange,
label = field.index.toString(),
isError = field.error,
)
DisableContextMenu {
state.wordFields.fastForEachIndexed { index, field ->
OutlineTextField(
value = field.word,
onValueChange = field.onChange,
label = field.index.toString(),
isError = field.error,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = if (index == state.wordFields.lastIndex) ImeAction.Done else ImeAction.Next,
),
keyboardActions = KeyboardActions(
onDone = { focusManager.clearFocus() },
onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) },
),
)
}
}
}
}

View file

@ -4,6 +4,7 @@ import androidx.compose.runtime.Stable
import com.tangem.core.decompose.di.ComponentScoped
import com.tangem.core.decompose.model.Model
import com.tangem.features.onboarding.v2.visa.impl.child.accesscode.ui.state.OnboardingVisaAccessCodeUM
import com.tangem.sdk.api.TangemSdkManager
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@ -13,6 +14,7 @@ import javax.inject.Inject
@ComponentScoped
internal class OnboardingVisaAccessCodeModel @Inject constructor(
override val dispatchers: CoroutineDispatcherProvider,
private val tangemSdkManager: TangemSdkManager,
) : Model() {
private val _uiState = MutableStateFlow(getInitialState())
@ -44,7 +46,7 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
_uiState.update { it.copy(step = OnboardingVisaAccessCodeUM.Step.ReEnter) }
}
OnboardingVisaAccessCodeUM.Step.ReEnter -> {
startActivationProcess()
startActivationProcess(accessCode = uiState.value.accessCodeFirst.text)
}
}
}
@ -67,8 +69,26 @@ internal class OnboardingVisaAccessCodeModel @Inject constructor(
return true
}
private fun startActivationProcess() {
TODO()
private fun startActivationProcess(accessCode: String) {
modelScope.launch {
@Suppress("UnusedPrivateMember")
val result = tangemSdkManager.activateVisaCard(
accessCode = accessCode,
challengeToSign = null,
activationInput = TODO(),
)
// when (result) {
// is CompletionResult.Success -> {
// val response = result.data
// TODO()
// }
// is CompletionResult.Failure -> {
// // show alert
// TODO()
// }
// }
}
}
private companion object {

View file

@ -2,14 +2,22 @@ package com.tangem.feature.onboarding.presentation.wallet2.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import com.tangem.core.ui.R
import com.tangem.core.ui.components.OutlineTextField
import com.tangem.core.ui.components.PrimaryButtonIconEnd
import com.tangem.core.ui.components.fields.contextmenu.DisableContextMenu
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.onboarding.presentation.wallet2.model.CheckSeedPhraseState
import com.tangem.feature.onboarding.presentation.wallet2.model.TextFieldState
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.Description
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingActionBlock
import com.tangem.feature.onboarding.presentation.wallet2.ui.components.OnboardingDescriptionBlock
@ -72,25 +80,31 @@ fun CheckSeedPhraseScreen(state: CheckSeedPhraseState, modifier: Modifier = Modi
@Composable
private fun CheckSeedPhraseBlock(state: CheckSeedPhraseState, modifier: Modifier = Modifier) {
Column(modifier) {
OutlineTextField(
value = state.tvSecondPhrase.textFieldValue,
onValueChange = state.tvSecondPhrase.onTextFieldValueChanged,
label = state.tvSecondPhrase.label,
isError = state.tvSecondPhrase.isError,
)
CheckSeedPhraseTextField(state = state.tvSecondPhrase, imeAction = ImeAction.Next)
CheckSeedPhraseTextField(state = state.tvSeventhPhrase, imeAction = ImeAction.Next)
CheckSeedPhraseTextField(state = state.tvEleventhPhrase, imeAction = ImeAction.Done)
}
}
OutlineTextField(
value = state.tvSeventhPhrase.textFieldValue,
onValueChange = state.tvSeventhPhrase.onTextFieldValueChanged,
label = state.tvSeventhPhrase.label,
isError = state.tvSeventhPhrase.isError,
)
@Composable
private fun CheckSeedPhraseTextField(state: TextFieldState, imeAction: ImeAction) {
val focusManager = LocalFocusManager.current
DisableContextMenu {
OutlineTextField(
value = state.tvEleventhPhrase.textFieldValue,
onValueChange = state.tvEleventhPhrase.onTextFieldValueChanged,
label = state.tvEleventhPhrase.label,
isError = state.tvEleventhPhrase.isError,
value = state.textFieldValue,
onValueChange = state.onTextFieldValueChanged,
label = state.label,
isError = state.isError,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
imeAction = imeAction,
),
keyboardActions = KeyboardActions(
onDone = { focusManager.clearFocus() },
onNext = { focusManager.moveFocus(focusDirection = FocusDirection.Down) },
),
)
}
}

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -15,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
@ -111,6 +113,10 @@ private fun PassphraseBlock(
label = stringResourceSafe(id = R.string.common_passphrase),
placeholder = stringResourceSafe(id = R.string.send_optional_field),
onIconClick = onPassphraseInfoClick,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
}
@ -135,6 +141,10 @@ private fun PhraseBlock(state: ImportSeedPhraseState, modifier: Modifier = Modif
style = SpanStyle(color = TangemTheme.colors.text.warning),
),
colors = TangemTextFieldsDefault.defaultTextFieldColors,
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
keyboardType = KeyboardType.Password,
),
)
Box(

View file

@ -70,13 +70,13 @@ internal class FeatureTogglesViewModel @Inject constructor(
titleResId = R.string.feature_toggles,
onBackClick = {},
refreshButton = TopBarWithRefreshUM.RefreshButton(
isVisible = false,
isVisible = !isMatchLocalConfig,
onRefreshClick = ::onRefreshClick,
),
)
} else {
uiState.topBar.copy(
refreshButton = uiState.topBar.refreshButton.copy(isVisible = isMatchLocalConfig),
refreshButton = uiState.topBar.refreshButton.copy(isVisible = !isMatchLocalConfig),
)
}
}

View file

@ -17,6 +17,7 @@ dependencies {
implementation(projects.domain.card)
implementation(projects.domain.legacy)
implementation(projects.domain.wallets.models)
implementation(projects.domain.visa.models)
implementation(projects.core.res)

View file

@ -15,6 +15,9 @@ import com.tangem.crypto.hdWallet.DerivationPath
import com.tangem.crypto.hdWallet.bip32.ExtendedPublicKey
import com.tangem.domain.models.scan.CardDTO
import com.tangem.domain.models.scan.ScanResponse
import com.tangem.domain.visa.model.VisaActivationInput
import com.tangem.domain.visa.model.VisaAuthChallenge
import com.tangem.domain.visa.model.VisaCardActivationResponse
import com.tangem.domain.wallets.models.UserWalletId
import com.tangem.operations.derivation.DerivationTaskResponse
import com.tangem.operations.preflightread.PreflightReadFilter
@ -141,4 +144,14 @@ interface TangemSdkManager {
fun clearProductType()
// endregion
// region Visa-specific
suspend fun activateVisaCard(
accessCode: String,
challengeToSign: VisaAuthChallenge.Card?,
activationInput: VisaActivationInput,
): CompletionResult<VisaCardActivationResponse>
// endregion
}