diff --git a/app/src/main/java/com/tangem/tap/MainActivity.kt b/app/src/main/java/com/tangem/tap/MainActivity.kt index 0d64c958e4..429cbe3e57 100644 --- a/app/src/main/java/com/tangem/tap/MainActivity.kt +++ b/app/src/main/java/com/tangem/tap/MainActivity.kt @@ -215,6 +215,13 @@ class MainActivity : AppCompatActivity(), SnackbarHandler, ActivityResultCallbac override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) + + /* + * FIXME: Test functionality. TangemSdk is null on some devices when HomeAction.Read is called + * inside IntentHandler. + */ + cardSdkLifecycleObserver.onCreate(context = this) + scope.launch { intentProcessor.handleIntent(intent) } diff --git a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt index d04cb9a62e..2717354cc4 100644 --- a/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt +++ b/app/src/main/java/com/tangem/tap/domain/tasks/product/ScanProductTask.kt @@ -3,6 +3,7 @@ package com.tangem.tap.domain.tasks.product import com.tangem.blockchain.common.Blockchain import com.tangem.common.CompletionResult import com.tangem.common.card.Card +import com.tangem.common.card.FirmwareVersion import com.tangem.common.core.CardSession import com.tangem.common.core.CardSessionRunnable import com.tangem.common.core.TangemError @@ -53,7 +54,7 @@ class ScanProductTask( } val cardDto = CardDTO(card) - val error = getErrorIfExcludedCard(cardDto) + val error = getErrorIfExcludedCard(cardDto, card) if (error != null) { callback(CompletionResult.Failure(error)) return @@ -83,9 +84,15 @@ class ScanProductTask( } } - private fun getErrorIfExcludedCard(cardDto: CardDTO): TangemError? { + private fun getErrorIfExcludedCard(cardDto: CardDTO, card: Card): TangemError? { if (cardDto.isExcluded) return TapSdkError.CardForDifferentApp if (cardDto.isNotSupportedInThatRelease) return TapSdkError.CardNotSupportedByRelease + // according ios decline card lower Ed25519Slip0010Available version and contains imported wallets + if (card.firmwareVersion < FirmwareVersion.Ed25519Slip0010Available && + card.wallets.any { it.isImported } + ) { + return TapSdkError.CardNotSupportedByRelease + } return null } } diff --git a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt index cddf55cc07..cd6161868b 100644 --- a/app/src/main/java/com/tangem/tap/features/BaseFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/BaseFragment.kt @@ -5,7 +5,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.activity.OnBackPressedCallback +import androidx.activity.addCallback import androidx.appcompat.widget.Toolbar import androidx.core.view.MenuHost import androidx.core.view.MenuProvider @@ -71,13 +71,11 @@ interface FragmentOnBackPressedHandler { @SuppressLint("FragmentBackPressedCallback") fun Fragment.addBackPressHandler(handler: FragmentOnBackPressedHandler) { - activity?.onBackPressedDispatcher?.addCallback( - this, - object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - handler.handleOnBackPressed() - } - }, - ) - view?.findViewById(R.id.toolbar)?.setNavigationOnClickListener { activity?.onBackPressed() } + requireActivity().onBackPressedDispatcher.addCallback { + handler.handleOnBackPressed() + } + + view?.findViewById(R.id.toolbar)?.setNavigationOnClickListener { + handler.handleOnBackPressed() + } } \ No newline at end of file diff --git a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt index 3b5e96c9e9..0a5151fd23 100644 --- a/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt +++ b/app/src/main/java/com/tangem/tap/features/customtoken/impl/presentation/viewmodels/AddCustomTokenViewModel.kt @@ -145,6 +145,7 @@ internal class AddCustomTokenViewModel @Inject constructor( fun createDerivationPathSelectorAdditionalItem( blockchain: Blockchain, type: DerivationPathSelectorType = DerivationPathSelectorType.BLOCKCHAIN, + derivationStyle: DerivationStyle?, ): SelectorItem.TitleWithSubtitle { return when (type) { DerivationPathSelectorType.DEFAULT -> SelectorItem.TitleWithSubtitle( @@ -160,7 +161,9 @@ internal class AddCustomTokenViewModel @Inject constructor( type = DerivationPathSelectorType.CUSTOM, ) DerivationPathSelectorType.BLOCKCHAIN -> SelectorItem.TitleWithSubtitle( - title = blockchain.derivationPath(DerivationStyle.LEGACY)?.rawPath?.let(TextReference::Str) + title = blockchain.derivationPath( + style = derivationStyle, + )?.rawPath?.let(TextReference::Str) ?: TextReference.Res(R.string.custom_token_derivation_path_default), subtitle = TextReference.Str(blockchain.fullName), blockchain = blockchain, @@ -251,9 +254,11 @@ internal class AddCustomTokenViewModel @Inject constructor( } private fun createDerivationPathsSelectorField(): AddCustomTokenSelectorField.DerivationPath? { - if (reduxStateHolder.scanResponse?.card?.settings?.isHDWalletAllowed == false) return null + val scanResponse = reduxStateHolder.scanResponse + if (scanResponse?.card?.settings?.isHDWalletAllowed == false) return null - val selectorItems = getDerivationPathsSelectorItems() + val selectorItems = + getDerivationPathsSelectorItems(scanResponse?.derivationStyleProvider?.getDerivationStyle()) return AddCustomTokenSelectorField.DerivationPath( label = TextReference.Res(R.string.custom_token_derivation_path_input_title), selectedItem = requireNotNull(selectorItems.firstOrNull()), @@ -263,22 +268,31 @@ internal class AddCustomTokenViewModel @Inject constructor( ) } - private fun getDerivationPathsSelectorItems(): List { + private fun getDerivationPathsSelectorItems( + derivationStyle: DerivationStyle?, + ): List { return listOf( createDerivationPathSelectorAdditionalItem( blockchain = Blockchain.Unknown, type = DerivationPathSelectorType.DEFAULT, + derivationStyle = derivationStyle, ), createDerivationPathSelectorAdditionalItem( blockchain = Blockchain.Unknown, type = DerivationPathSelectorType.CUSTOM, + derivationStyle = derivationStyle, ), ) + Blockchain.values() .filter { blockchain -> blockchain.isSupportedInApp() && !blockchain.isTestnet() } .sortedBy(Blockchain::fullName) - .map(::createDerivationPathSelectorAdditionalItem) + .map { + createDerivationPathSelectorAdditionalItem( + blockchain = it, + derivationStyle = derivationStyle, + ) + } } private fun createDerivationPathInputField(): AddCustomTokenInputField.DerivationPath? { @@ -782,6 +796,7 @@ internal class AddCustomTokenViewModel @Inject constructor( } fun onResetButtonClick() { + val scanResponse = reduxStateHolder.scanResponse with(uiState.form) { uiState = uiState.copySealed( form = uiState.form.copy( @@ -799,11 +814,11 @@ internal class AddCustomTokenViewModel @Inject constructor( decimalsInputField = decimalsInputField.copy(value = "", isEnabled = false), derivationPathSelectorField = derivationPathSelectorField?.copy( isEnabled = true, - selectedItem = formStateBuilder - .createDerivationPathSelectorAdditionalItem( - blockchain = Blockchain.Unknown, - type = DerivationPathSelectorType.DEFAULT, - ), + selectedItem = formStateBuilder.createDerivationPathSelectorAdditionalItem( + blockchain = Blockchain.Unknown, + type = DerivationPathSelectorType.DEFAULT, + derivationStyle = scanResponse?.derivationStyleProvider?.getDerivationStyle(), + ), ), ), ) diff --git a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt index 5b6afc478f..49f78995bc 100644 --- a/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt +++ b/app/src/main/java/com/tangem/tap/features/onboarding/products/wallet/ui/OnboardingWalletFragment.kt @@ -97,8 +97,7 @@ class OnboardingWalletFragment : (activity as? AppCompatActivity)?.setSupportActionBar(binding.toolbar) - binding.toolbar.setNavigationOnClickListener { activity?.onBackPressed() } - addBackPressHandler(this) + addBackPressHandler(handler = this) store.dispatch(OnboardingWallet2Action.Init(seedPhraseViewModel.maxProgress)) store.dispatch(OnboardingWalletAction.Init) diff --git a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt index 7c12e82496..6de86eba68 100644 --- a/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt +++ b/app/src/main/java/com/tangem/tap/features/send/ui/stateSubscribers/SendStateSubscriber.kt @@ -261,19 +261,34 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber } @Suppress("MagicNumber") - private fun handleFeeState(fg: SendFragment, state: FeeState) = with(fg.binding.clNetworkFee) { - fg.view?.findViewById(R.id.clNetworkFee)?.show(state.mainLayoutIsVisible) - flExpandCollapse.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f - llFeeControlsContainer.show(state.controlsLayoutIsVisible) - chipGroup.show(state.feeChipGroupIsVisible) + private fun handleFeeState(fg: SendFragment, state: FeeState) { + with(fg.binding.clNetworkFee) { + fg.view?.findViewById(R.id.clNetworkFee)?.show(state.mainLayoutIsVisible) + flExpandCollapse.imvExpandCollapse.rotation = if (state.controlsLayoutIsVisible) 0f else 180f + llFeeControlsContainer.show(state.controlsLayoutIsVisible) + chipGroup.show(state.feeChipGroupIsVisible) - swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled - if (swIncludeFee.isChecked != state.feeIsIncluded) { - swIncludeFee.isChecked = state.feeIsIncluded + swIncludeFee.isEnabled = state.includeFeeSwitcherIsEnabled + if (swIncludeFee.isChecked != state.feeIsIncluded) { + swIncludeFee.isChecked = state.feeIsIncluded + } + + val chipId = FeeUiHelper.toId(state.selectedFeeType) + if (chipGroup.checkedChipId != chipId && chipId != View.NO_ID) chipGroup.check(chipId) + } + with(fg.binding.clReceiptContainer) { + when (state.progressState) { + ProgressState.Loading -> { + tvReceiptFeeValue.hide() + pbReceiptFee.show() + } + ProgressState.Done -> { + pbReceiptFee.hide() + tvReceiptFeeValue.show() + } + else -> {} + } } - - val chipId = FeeUiHelper.toId(state.selectedFeeType) - if (chipGroup.checkedChipId != chipId && chipId != View.NO_ID) chipGroup.check(chipId) } @Suppress("LongMethod", "ComplexMethod") diff --git a/core/res/src/main/res/values-ru/strings.xml b/core/res/src/main/res/values-ru/strings.xml index 97aae433d7..fb0916e031 100644 --- a/core/res/src/main/res/values-ru/strings.xml +++ b/core/res/src/main/res/values-ru/strings.xml @@ -99,7 +99,7 @@ Сохранить изменения Искать Поиск токенов - Секретная фраза + Seed-фраза Выберите действие Продать Отправить @@ -249,18 +249,18 @@ Код доступа Подключиться Резервная копия - Прочитать о секретной фразе + Прочитать о seed-фразе Запишите эти 12 слов в порядке, указанном ниже, и сохраните их в надежном месте. - Ваша секретная фраза - Чтобы импортировать кошелек, введите секретную фразу в поле ниже - Cоздать секретную фразу + Ваша seed-фраза + Чтобы импортировать кошелек, введите seed-фразу в поле ниже + Создать seed-фразу Импорт кошелька - Секретная фраза — это набор слов, который дает возможность восстановить кошелек. В отличие от ключей, сгенерированных картой, seed-фраза не защищена и может быть скопирована и украдена. Используйте этот вариант на свой страх и риск. - Использовать секретную фразу - Неверная секретная фраза. Пожалуйста, проверьте порядок слов. - Неверная секретная фраза. Пожалуйста, проверьте орфографию. + Seed-фраза — это набор слов, который дает возможность восстановить кошелек. В отличие от ключей, сгенерированных картой, seed-фраза не защищена и может быть скопирована и украдена. Используйте этот вариант на свой страх и риск. + Использовать seed-фразу + Неверная seed-фраза. Пожалуйста, проверьте порядок слов. + Неверная seed-фраза. Пожалуйста, проверьте орфографию. Устаревший - Чтобы проверить, правильно ли вы записали секретную фразу, введите 2-е, 7-е и 11-е слова + Чтобы проверить, правильно ли вы записали seed-фразу, введите 2-е, 7-е и 11-е слова Итак, проверим Для начала работы просто запросите начисление wxDAI на свой кошелек Это займет несколько секунд @@ -567,6 +567,6 @@ Это необратимое действие Войти с %s Сканировать карту - Используйте %s или код доступа для входа в приложение + Используйте %s или отсканируйте карту для входа в приложение C возвращением! diff --git a/core/res/src/main/res/values-zh-rTW/strings.xml b/core/res/src/main/res/values-zh-rTW/strings.xml index 20f0e81a58..ad55e197d1 100644 --- a/core/res/src/main/res/values-zh-rTW/strings.xml +++ b/core/res/src/main/res/values-zh-rTW/strings.xml @@ -215,16 +215,10 @@ 創建備份 閱讀更多關於助記詞的訊息 按照下面給出的順序寫下這 12 個單詞,並將它們存放在安全秘密的地方。 - 您的助記詞 - 要導入您的錢包,請在下面的字段中輸入您的助記詞 生成助記詞 導入錢包 - 註記詞是一系列能夠恢復錢包的單詞。與卡片生成的密鑰不同,助記詞不受保護,可以被複製和竊取。使用此選項需要您自擔風險。 使用助記詞 - 無效的助記詞。請檢查單詞順序。 - 助記詞無效。請檢查您的拼寫。 Legacy - 為了檢查您是否正確輸入了助記詞,請輸入第 2、7 和 11 個單詞 那麼,讓我們檢查一下 要開始,只需將 wxDAI 獲取到您的錢包 這會花上幾秒 diff --git a/core/res/src/main/res/values/strings.xml b/core/res/src/main/res/values/strings.xml index 8275351d0b..a4bc3c019a 100644 --- a/core/res/src/main/res/values/strings.xml +++ b/core/res/src/main/res/values/strings.xml @@ -231,7 +231,7 @@ Skip for later How does it work? Let\'s generate all the keys on your card and create a secure wallet - Create wallet + Create a wallet Create a wallet Other options Your keys will be securely generated inside the card. There is no seed phrase, which means nobody can export or steal it. @@ -245,18 +245,18 @@ Pin code Connect Creating a backup - Read more about seed phrases + Read more about seed phrase Write these 12 words down in the order given below and store them in a safe and secret place. - Your secret phrase - To import your wallet, enter your secret phrase in the field below + Your seed phrase + To import your wallet, enter your seed phrase in the field below Generate seed phrase Import wallet - A secret phrase is a series of words that allows you to recover your wallet. Unlike the keys generated by the card, secret phrases are unprotected and can be copied and stolen. Use this option at your own risk. + A seed phrase is a series of words that allows you to recover your wallet. Unlike the keys generated by the card, seed phrases are unprotected and can be copied and stolen. Use this option at your own risk. Use seed phrase - Invalid secret phrase. Please check the word order. - Invalid secret phrase. Please check your spelling. + Invalid seed phrase. Please check the word order. + Invalid seed phrase. Please check your spelling. Legacy - To check whether you’ve written down your secret phrase correctly, please enter the 2nd, 7th and 11th words + To check whether you’ve written down your seed phrase correctly, please enter the 2nd, 7th and 11th words So, let’s check To get started, simply claim wxDAI to your wallet It will take a few seconds diff --git a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt index 522b4d2b9b..993a613c73 100644 --- a/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt +++ b/data/card/src/main/java/com/tangem/data/card/sdk/DefaultCardSdkProvider.kt @@ -24,7 +24,9 @@ internal class DefaultCardSdkProvider @Inject constructor() : CardSdkProvider, C private var _sdk: TangemSdk? = null override fun onCreate(context: Context) { - _sdk = TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config) + if (_sdk == null) { + _sdk = TangemSdk.initWithBiometrics(activity = context as FragmentActivity, config = config) + } } override fun onDestroy() { diff --git a/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt b/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt index 73c31f5a28..c30c02b77c 100644 --- a/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt +++ b/domain/legacy/src/main/java/com/tangem/domain/common/TwinsHelper.kt @@ -51,17 +51,19 @@ object TwinsHelper { * cb64 <-> cb65 */ fun isTwinsCompatible(firstCardId: String, secondCardId: String): Boolean { - if (firstCardId.startsWith(FIRST_CARD_FIRST_SERIES) && - secondCardId.startsWith(SECOND_CARD_FIRST_SERIES) - ) { - return true + return secondCardId.startsWith(getCompatibleTwinCardId(firstCardId)) + } + + private fun getCompatibleTwinCardId(cardId: String): String { + return when { + cardId.startsWith(FIRST_CARD_FIRST_SERIES) -> SECOND_CARD_FIRST_SERIES + cardId.startsWith(FIRST_CARD_SECOND_SERIES) -> SECOND_CARD_SECOND_SERIES + cardId.startsWith(SECOND_CARD_FIRST_SERIES) -> FIRST_CARD_FIRST_SERIES + cardId.startsWith(SECOND_CARD_SECOND_SERIES) -> FIRST_CARD_SECOND_SERIES + else -> { + "unknown" + } } - if (firstCardId.startsWith(FIRST_CARD_SECOND_SERIES) && - secondCardId.startsWith(SECOND_CARD_SECOND_SERIES) - ) { - return true - } - return false } } diff --git a/domain/legacy/src/test/java/com/tangem/domain/common/TwinsHelperTest.kt b/domain/legacy/src/test/java/com/tangem/domain/common/TwinsHelperTest.kt new file mode 100644 index 0000000000..2fc624d9b8 --- /dev/null +++ b/domain/legacy/src/test/java/com/tangem/domain/common/TwinsHelperTest.kt @@ -0,0 +1,50 @@ +package com.tangem.domain.common + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class TwinsHelperTest { + + private val pack1Twins = listOf( + "CB610000021", + "CB620000031", + ) + + private val pack2Twins = listOf( + "CB640000012", + "CB650000011", + ) + + @Test + fun `twins compatibility pack 1 success`() { + assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack1Twins[1])) + assertTrue(TwinsHelper.isTwinsCompatible(pack1Twins[1], pack1Twins[0])) + } + + @Test + fun `twins compatibility pack 1 same cards`() { + assertFalse(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack1Twins[0])) + assertFalse(TwinsHelper.isTwinsCompatible(pack1Twins[1], pack1Twins[1])) + } + + @Test + fun `twins compatibility pack 2 success`() { + assertTrue(TwinsHelper.isTwinsCompatible(pack2Twins[0], pack2Twins[1])) + assertTrue(TwinsHelper.isTwinsCompatible(pack2Twins[1], pack2Twins[0])) + } + + @Test + fun `twins compatibility pack 2 same cards`() { + assertFalse(TwinsHelper.isTwinsCompatible(pack2Twins[0], pack2Twins[0])) + assertFalse(TwinsHelper.isTwinsCompatible(pack2Twins[1], pack2Twins[1])) + } + + @Test + fun `twins compatibility false for different packs`() { + assertFalse(TwinsHelper.isTwinsCompatible(pack2Twins[0], pack1Twins[1])) + assertFalse(TwinsHelper.isTwinsCompatible(pack1Twins[1], pack2Twins[0])) + assertFalse(TwinsHelper.isTwinsCompatible(pack1Twins[0], pack2Twins[1])) + assertFalse(TwinsHelper.isTwinsCompatible(pack2Twins[1], pack1Twins[0])) + } +} \ No newline at end of file diff --git a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt index 6c46276300..970edc26f5 100644 --- a/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt +++ b/domain/tokens/models/src/main/java/com/tangem/domain/tokens/models/CryptoCurrency.kt @@ -135,7 +135,7 @@ sealed class CryptoCurrency : Serializable { } } - sealed class StandardType { + sealed class StandardType : Serializable { abstract val name: String object ERC20 : StandardType() { diff --git a/gradle/dependencies.toml b/gradle/dependencies.toml index fadee9849b..f115058405 100644 --- a/gradle/dependencies.toml +++ b/gradle/dependencies.toml @@ -80,9 +80,9 @@ okHttp-prettyLogging = "3.1.0" # endregion Other libraries # region Tangem -tangemBlockchainSdk = "release-app_4.10-324" +tangemBlockchainSdk = "release-app_4.10-328" #tangemBlockchainSdk = "0.0.1" # Keep it! - used for local builds -tangemCardSdk = "release-app_4.10-290" +tangemCardSdk = "release-app_4.10-292" #tangemCardSdk = "0.0.1" # Keep it! - used for local builds # endregion Tangem