From b40cc1c2659069ce435558db8436046a166a3602 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 15 Jan 2025 16:56:12 +0200 Subject: [PATCH 1/5] Updated on 2026-08-14 --- app/build.gradle.kts | 19 +++++++++++++++++++ app/src/main/assets/tangem-app-config | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 8cf12cc08e..9c4e5fbd00 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/assets/tangem-app-config b/app/src/main/assets/tangem-app-config index e91a299285..c36c77faf2 160000 --- a/app/src/main/assets/tangem-app-config +++ b/app/src/main/assets/tangem-app-config @@ -1 +1 @@ -Subproject commit e91a2992857afa5cefce05c2750daf175b783603 +Subproject commit c36c77faf2101b30f472b2d5ba00dc0b48af9baa From 78cf8265a6deb6aa8477d83c89958ee3ff7b2452 Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 15 Jan 2025 23:01:11 +0700 Subject: [PATCH 2/5] Updated on 2026-08-14 --- .../tangem/domain/walletmanager/DefaultWalletManagersFacade.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt index 7c2147cd08..440e818d8f 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/walletmanager/DefaultWalletManagersFacade.kt @@ -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, From 70b8d28e1819b5221df82be3b272bf0250086bef Mon Sep 17 00:00:00 2001 From: Tangem Date: Wed, 15 Jan 2025 22:26:58 +0400 Subject: [PATCH 3/5] Updated on 2026-08-14 --- .../featuretoggles/viewmodels/FeatureTogglesViewModel.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt index 0c85993387..95338c3250 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/featuretoggles/viewmodels/FeatureTogglesViewModel.kt @@ -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), ) } } From 89b51cc7e0e611bce3e367ded8fd12c5c7af067d Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Jan 2025 09:38:11 +0300 Subject: [PATCH 4/5] Updated on 2026-08-14 --- .../fields/contextmenu/DisableContextMenu.kt | 38 +++++++++++++++ .../ui/MultiWalletSeedPhraseImport.kt | 12 ++++- .../ui/MultiWalletSeedPhraseWordsCheck.kt | 35 +++++++++++--- .../wallet2/ui/CheckSeedPhraseScreen.kt | 46 ++++++++++++------- .../wallet2/ui/ImportSeedPhraseScreen.kt | 10 ++++ 5 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 core/ui/src/main/java/com/tangem/core/ui/components/fields/contextmenu/DisableContextMenu.kt diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/fields/contextmenu/DisableContextMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/components/fields/contextmenu/DisableContextMenu.kt new file mode 100644 index 0000000000..bf8c61037a --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/components/fields/contextmenu/DisableContextMenu.kt @@ -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 +} \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt index 5512fd5d07..f64fd62c0d 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseImport.kt @@ -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( diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt index 9d37d02677..dfc2b1cd70 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/multiwallet/impl/child/seedphrase/ui/MultiWalletSeedPhraseWordsCheck.kt @@ -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) }, + ), + ) + } } } } diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt index b9ee1576da..bf29010072 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/CheckSeedPhraseScreen.kt @@ -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) }, + ), ) } } \ No newline at end of file diff --git a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt index 8f9c0f7056..bc6e59c4dd 100644 --- a/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt +++ b/features/onboarding/src/main/java/com/tangem/feature/onboarding/presentation/wallet2/ui/ImportSeedPhraseScreen.kt @@ -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( From ea087aac495a45cbb1107a6188729fd77720a255 Mon Sep 17 00:00:00 2001 From: Tangem Date: Thu, 16 Jan 2025 10:07:05 +0300 Subject: [PATCH 5/5] Updated on 2026-08-14 --- .../tangem/tap/data/DefaultVisaOTPStorage.kt | 42 +++ .../tangem/tap/di/TangemSdkManagerModule.kt | 3 + .../tangem/tap/di/data/VisaStorageModule.kt | 6 + .../sdk/impl/DefaultTangemSdkManager.kt | 27 ++ .../domain/sdk/impl/MockTangemSdkManager.kt | 13 + .../tasks/visa/VisaCardActivationTask.kt | 268 ++++++++++++++++++ .../di/UserWalletsListManagerModule.kt | 2 +- .../com/tangem/datasource/di/MoshiModule.kt | 24 +- .../datasource/local/visa/VisaOTPStorage.kt | 12 + .../visa/DefaultVisaActivationRepository.kt | 5 + .../ScanResponseAsStringSerializer.kt | 2 +- .../domain/visa/model/ActivationOrder.kt | 5 + .../visa/model/SignedActivationOrder.kt | 6 + .../domain/visa/model/VisaActivationError.kt | 2 + .../visa/model/VisaCardActivationResponse.kt | 6 + .../visa/model/VisaCardActivationStatus.kt | 2 +- .../tangem/domain/visa/model/VisaRootOTP.kt | 4 + .../repository/VisaActivationRepository.kt | 3 + .../model/OnboardingVisaAccessCodeModel.kt | 26 +- libs/tangem-sdk-api/build.gradle.kts | 1 + .../com/tangem/sdk/api/TangemSdkManager.kt | 13 + 21 files changed, 464 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt create mode 100644 app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt create mode 100644 core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaOTPStorage.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt create mode 100644 domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt diff --git a/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt new file mode 100644 index 0000000000..85c60d494d --- /dev/null +++ b/app/src/main/java/com/tangem/tap/data/DefaultVisaOTPStorage.kt @@ -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_" + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt index d943172df9..50d0faa062 100644 --- a/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/di/TangemSdkManagerModule.kt @@ -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, ) } } diff --git a/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt b/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt index 7d04ce793d..c78909a676 100644 --- a/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt +++ b/app/src/main/java/com/tangem/tap/di/data/VisaStorageModule.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt index d7f1cd4f90..755b770c22 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/DefaultTangemSdkManager.kt @@ -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 { + 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 } diff --git a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt index 0d454c11e8..c0dd2c2c03 100644 --- a/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt +++ b/app/src/main/java/com/tangem/tap/domain/sdk/impl/MockTangemSdkManager.kt @@ -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 { + error("Not implemented") + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt new file mode 100644 index 0000000000..184e2a3f63 --- /dev/null +++ b/app/src/main/java/com/tangem/tap/domain/tasks/visa/VisaCardActivationTask.kt @@ -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 { + + override fun run(session: CardSession, callback: CompletionCallback) { + coroutineScope.launch { + callback(runSuspend(session)) + } + } + + private suspend fun runSuspend(session: CardSession): CompletionResult { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt index baff7e0289..c0d95f3122 100644 --- a/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt +++ b/app/src/main/java/com/tangem/tap/domain/userWalletList/di/UserWalletsListManagerModule.kt @@ -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() diff --git a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt index 0a3b4fef73..0294c9d76a 100644 --- a/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt +++ b/core/datasource/src/main/java/com/tangem/datasource/di/MoshiModule.kt @@ -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 diff --git a/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaOTPStorage.kt b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaOTPStorage.kt new file mode 100644 index 0000000000..8395982e69 --- /dev/null +++ b/core/datasource/src/main/java/com/tangem/datasource/local/visa/VisaOTPStorage.kt @@ -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 \ No newline at end of file diff --git a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt index 5bab1a1690..a5be656c53 100644 --- a/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt +++ b/data/visa/src/main/kotlin/com/tangem/data/visa/DefaultVisaActivationRepository.kt @@ -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") + } } \ No newline at end of file diff --git a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt index 3d8a7b4229..7f22e52275 100644 --- a/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt +++ b/domain/models/src/main/kotlin/com/tangem/domain/models/scan/serialization/ScanResponseAsStringSerializer.kt @@ -23,7 +23,7 @@ internal object ScanResponseAsStringSerializer : KSerializer { .add(TangemSdkAdapter.DateAdapter()) .add(TangemSdkAdapter.DerivationNodeAdapter()) .add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model - .add(VisaCardActivationStatus.serializer) + .add(VisaCardActivationStatus.jsonAdapter) .add(KotlinJsonAdapterFactory()) .build() diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt new file mode 100644 index 0000000000..a5924d0da4 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/ActivationOrder.kt @@ -0,0 +1,5 @@ +package com.tangem.domain.visa.model + +data class ActivationOrder( + val hash: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt new file mode 100644 index 0000000000..9c16d85bfc --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/SignedActivationOrder.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +data class SignedActivationOrder( + val activationOrder: ActivationOrder, + val signature: String, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt index 410b9f0b52..11685b3866 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaActivationError.kt @@ -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"), } \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt new file mode 100644 index 0000000000..79cc4c9a95 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationResponse.kt @@ -0,0 +1,6 @@ +package com.tangem.domain.visa.model + +data class VisaCardActivationResponse( + val signedActivationOrder: SignedActivationOrder, + val rootOTP: VisaRootOTP, +) \ No newline at end of file diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt index 40921403dd..7e1a10a1c8 100644 --- a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaCardActivationStatus.kt @@ -28,7 +28,7 @@ sealed class VisaCardActivationStatus { data object Blocked : VisaCardActivationStatus() companion object { - val serializer: PolymorphicJsonAdapterFactory + val jsonAdapter: PolymorphicJsonAdapterFactory get() = PolymorphicJsonAdapterFactory.of(VisaCardActivationStatus::class.java, "type") .withSubtype(Activated::class.java, "Activated") .withSubtype(ActivationStarted::class.java, "ActivationStarted") diff --git a/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt new file mode 100644 index 0000000000..310ee01021 --- /dev/null +++ b/domain/visa/models/src/main/kotlin/com/tangem/domain/visa/model/VisaRootOTP.kt @@ -0,0 +1,4 @@ +package com.tangem.domain.visa.model + +@JvmInline +value class VisaRootOTP(val value: String) \ No newline at end of file diff --git a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt index 12db7d7bbe..8127f921c6 100644 --- a/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt +++ b/domain/visa/src/main/kotlin/com/tangem/domain/visa/repository/VisaActivationRepository.kt @@ -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 } \ No newline at end of file diff --git a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt index 9f99596cfb..24d171ebb9 100644 --- a/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt +++ b/features/onboarding-v2/impl/src/main/kotlin/com/tangem/features/onboarding/v2/visa/impl/child/accesscode/model/OnboardingVisaAccessCodeModel.kt @@ -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 { diff --git a/libs/tangem-sdk-api/build.gradle.kts b/libs/tangem-sdk-api/build.gradle.kts index 91756f3226..514702adfa 100644 --- a/libs/tangem-sdk-api/build.gradle.kts +++ b/libs/tangem-sdk-api/build.gradle.kts @@ -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) diff --git a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt index a2c6cc8c4f..d86b31b3d0 100644 --- a/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt +++ b/libs/tangem-sdk-api/src/main/kotlin/com/tangem/sdk/api/TangemSdkManager.kt @@ -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 + + // endregion } \ No newline at end of file