Updated on 2026-08-14

This commit is contained in:
Tangem 2023-05-10 16:07:57 +03:00
commit b6f4d67628
19 changed files with 179 additions and 11 deletions

View file

@ -5,6 +5,7 @@ import com.tangem.TangemSdk
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.Wallet
import com.tangem.common.CompletionResult
import com.tangem.domain.common.TapWorkarounds.isTangemTwins
import com.tangem.domain.models.scan.CardDTO
import com.tangem.tap.domain.tasks.SignHashesTask
import kotlinx.coroutines.suspendCancellableCoroutine
@ -23,12 +24,12 @@ class TangemSigner(
publicKey: Wallet.PublicKey,
): CompletionResult<List<ByteArray>> {
return suspendCancellableCoroutine { continuation ->
val cardId = if (card.backupStatus?.isActive == true) null else card.cardId
val isCardNotBackedUp = card.backupStatus?.isActive != true && !card.isTangemTwins
val task = SignHashesTask(hashes, publicKey)
tangemSdk.startSessionWithRunnable(
runnable = task,
cardId = cardId,
cardId = card.cardId.takeIf { isCardNotBackedUp },
initialMessage = initialMessage,
accessCode = accessCode,
) { result ->

View file

@ -4,6 +4,7 @@ import com.tangem.common.core.TangemError
import com.tangem.wallet.R
sealed class UserWalletsListError(code: Int) : TangemError(code) {
override val silent: Boolean
get() = (cause as? TangemError)?.silent == true
@ -18,6 +19,10 @@ sealed class UserWalletsListError(code: Int) : TangemError(code) {
override var customMessage: String = "Encryption key invalidated"
}
object BiometricsAuthenticationDisabled : UserWalletsListError(code = 60005) {
override var customMessage: String = "Biometrics authentication disabled"
}
data class BiometricsAuthenticationLockout(val isPermanent: Boolean) : UserWalletsListError(code = 60003) {
override var customMessage: String = "Biometric authentication lockout, permanent: $isPermanent"
}

View file

@ -17,6 +17,7 @@ import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUse
import com.tangem.tap.domain.userWalletList.repository.implementation.DefaultUserWalletsSensitiveInformationRepository
import com.tangem.tap.domain.userWalletList.utils.json.ByteArrayKeyAdapter
import com.tangem.tap.domain.userWalletList.utils.json.CardBackupStatusAdapter
import com.tangem.tap.domain.userWalletList.utils.json.DerivationPathAdapterWithMigration
import com.tangem.tap.domain.userWalletList.utils.json.ExtendedPublicKeysMapAdapter
import com.tangem.tap.domain.userWalletList.utils.json.ScanResponseDerivedKeysMapAdapter
import com.tangem.tap.domain.userWalletList.utils.json.WalletDerivedKeysMapAdapter
@ -33,8 +34,8 @@ fun UserWalletsListManager.Companion.provideBiometricImplementation(
.add(ByteArrayKeyAdapter())
.add(ExtendedPublicKeysMapAdapter())
.add(CardBackupStatusAdapter())
.add(DerivationPathAdapterWithMigration())
.add(TangemSdkAdapter.DateAdapter())
.add(TangemSdkAdapter.DerivationPathAdapter())
.add(TangemSdkAdapter.DerivationNodeAdapter())
.add(TangemSdkAdapter.FirmwareVersionAdapter()) // For PrimaryCard model
.add(KotlinJsonAdapterFactory())

View file

@ -48,6 +48,8 @@ internal class BiometricUserWalletsKeysRepository(
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true)
is TangemSdkError.BiometricCryptographyKeyInvalidated ->
UserWalletsListError.EncryptionKeyInvalidated
is TangemSdkError.BiometricsUnavailable ->
UserWalletsListError.BiometricsAuthenticationDisabled
else -> error
}
}

View file

@ -0,0 +1,35 @@
package com.tangem.tap.domain.userWalletList.utils.json
import com.squareup.moshi.FromJson
import com.squareup.moshi.ToJson
import com.tangem.common.extensions.guard
import com.tangem.common.json.MoshiJsonConverter
import com.tangem.crypto.hdWallet.DerivationNode
import com.tangem.crypto.hdWallet.DerivationPath
import timber.log.Timber
class DerivationPathAdapterWithMigration {
@ToJson
fun toJson(src: DerivationPath): String = src.rawPath
@FromJson
fun fromJson(json: String): DerivationPath {
val jsonMap = MoshiJsonConverter.default().toMap(json)
return if (jsonMap.isEmpty()) {
DerivationPath(json)
} else {
fromLegacyScheme(jsonMap)
}
}
@Suppress("UNCHECKED_CAST")
private fun fromLegacyScheme(jsonMap: Map<String, Any>): DerivationPath {
val rawPath = jsonMap["rawPath"] as String
val nodeIndexes = (jsonMap["nodes"] as? List<Number>).guard {
Timber.e("Unable to convert derivation path nodes from JSON")
return DerivationPath(rawPath)
}
val nodes = nodeIndexes.map { DerivationNode.fromIndex(it.toLong()) }
return DerivationPath(rawPath, nodes)
}
}

View file

@ -160,7 +160,10 @@ fun DetailsItem(item: SettingsElement, appCurrency: String, onItemsClick: () ->
@Composable
fun TangemSocialAccounts(links: List<SocialNetworkLink>, onSocialNetworkClick: (SocialNetworkLink) -> Unit) {
LazyRow(modifier = Modifier.padding(start = 8.dp, end = 8.dp)) {
LazyRow(
modifier = Modifier.padding(start = 8.dp, end = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
items(links) {
Icon(
painter = painterResource(id = it.network.iconRes),

View file

@ -55,25 +55,28 @@ sealed class SocialNetwork(val id: String, val iconRes: Int) {
object GitHub : SocialNetwork("GitHub", R.drawable.ic_github)
object YouTube : SocialNetwork("YouTube", R.drawable.ic_youtube)
object LinkedIn : SocialNetwork("LinkedIn", R.drawable.ic_linkedin)
object Discord : SocialNetwork("Discord", R.drawable.ic_discord)
}
object TangemSocialAccounts {
val accountsEn: List<SocialNetworkLink> = listOf(
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/TangemCards"),
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat"),
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"),
)
val accountsRu: List<SocialNetworkLink> = listOf(
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_ru"),
SocialNetworkLink(SocialNetwork.Telegram, "https://t.me/tangem_chat_ru"),
SocialNetworkLink(SocialNetwork.Twitter, "https://twitter.com/tangem"),
SocialNetworkLink(SocialNetwork.Facebook, "https://m.facebook.com/TangemCards/"),
SocialNetworkLink(SocialNetwork.Instagram, "https://instagram.com/tangemcards"),
SocialNetworkLink(SocialNetwork.GitHub, "https://github.com/tangem"),
SocialNetworkLink(SocialNetwork.YouTube, "https://youtube.com/channel/UCFGwLS7yggzVkP6ozte0m1w"),
SocialNetworkLink(SocialNetwork.LinkedIn, "https://www.linkedin.com/company/tangem"),
SocialNetworkLink(SocialNetwork.Discord, "https://discord.gg/7AqTVyqdGS"),
)
}

View file

@ -25,6 +25,7 @@ import com.tangem.core.ui.fragments.ComposeBottomSheetFragment
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.common.analytics.events.MyWallets
import com.tangem.tap.features.details.ui.cardsettings.resolveReference
import com.tangem.tap.features.walletSelector.ui.components.BiometricsDisabledWarningContent
import com.tangem.tap.features.walletSelector.ui.components.BiometricsLockoutWarningContent
import com.tangem.tap.features.walletSelector.ui.components.KeyInvalidatedWarningContent
import com.tangem.tap.features.walletSelector.ui.components.RemoveWalletDialogContent
@ -96,6 +97,7 @@ internal class WalletSelectorBottomSheetFragment : ComposeBottomSheetFragment<Wa
is DialogModel.RenameWalletDialog -> RenameWalletDialogContent(dialog)
is WarningModel.BiometricsLockoutWarning -> BiometricsLockoutWarningContent(dialog)
is WarningModel.KeyInvalidatedWarning -> KeyInvalidatedWarningContent(dialog)
is WarningModel.BiometricsDisabledWarning -> BiometricsDisabledWarningContent(dialog)
}
}
}

View file

@ -199,6 +199,10 @@ internal class WalletSelectorViewModel : ViewModel(), StoreSubscriber<WalletSele
is UserWalletsListError.EncryptionKeyInvalidated -> WarningModel.KeyInvalidatedWarning(
onDismiss = this::dismissWarningDialog,
)
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
onConfirm = { /* [REDACTED_TODO_COMMENT] */ },
onDismiss = this::dismissWarningDialog,
)
else -> currentDialog
}
}

View file

@ -0,0 +1,55 @@
package com.tangem.tap.features.walletSelector.ui.components
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.components.BasicDialog
import com.tangem.core.ui.components.DialogButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.tap.features.walletSelector.ui.model.WarningModel
import com.tangem.wallet.R
@Composable
internal fun BiometricsDisabledWarningContent(warning: WarningModel.BiometricsDisabledWarning) {
BasicDialog(
title = stringResource(id = R.string.common_warning),
message = stringResource(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onDismiss,
),
)
}
// region Preview
@Composable
private fun BiometricsDisabledWarningContentSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
BiometricsDisabledWarningContent(
warning = WarningModel.BiometricsDisabledWarning(
onConfirm = {},
onDismiss = {},
),
)
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsDisabledWarningContentPreview_Light() {
TangemTheme {
BiometricsDisabledWarningContentSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricsDisabledWarningContentPreview_Dark() {
TangemTheme(isDark = true) {
BiometricsDisabledWarningContentSample()
}
}
// endregion Preview

View file

@ -22,4 +22,9 @@ internal sealed interface WarningModel : DialogModel {
data class KeyInvalidatedWarning(
val onDismiss: () -> Unit,
) : WarningModel
data class BiometricsDisabledWarning(
val onConfirm: () -> Unit,
val onDismiss: () -> Unit,
) : WarningModel
}

View file

@ -71,6 +71,10 @@ internal class WelcomeViewModel : ViewModel(), StoreSubscriber<WelcomeState> {
is UserWalletsListError.EncryptionKeyInvalidated -> WarningModel.KeyInvalidatedWarning(
onDismiss = this::dismissWarning,
)
is UserWalletsListError.BiometricsAuthenticationDisabled -> WarningModel.BiometricsDisabledWarning(
onConfirm = { /* [REDACTED_TODO_COMMENT] */ },
onDismiss = this::dismissWarning,
)
else -> null
}
}

View file

@ -43,6 +43,17 @@ internal fun WarningDialog(warning: WarningModel?) {
),
)
}
is WarningModel.BiometricsDisabledWarning -> {
BasicDialog(
title = stringResource(id = R.string.common_warning),
message = stringResource(id = R.string.biometric_unavailable_warning),
onDismissDialog = warning.onDismiss,
confirmButton = DialogButton(
title = stringResource(id = R.string.common_ok),
onClick = warning.onConfirm,
),
)
}
}
}
@ -103,7 +114,6 @@ private fun BiometricsLockoutDialog_Permanent_Preview_Dark() {
}
}
// region Preview
@Composable
private fun KeyInvalidatedWarningSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
@ -126,5 +136,27 @@ private fun KeyInvalidatedWarningPreview_Dark() {
KeyInvalidatedWarningSample()
}
}
// endregion Preview
@Composable
private fun BiometricDisabledWarningSample(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
WarningDialog(warning = WarningModel.BiometricsDisabledWarning(onConfirm = {}, onDismiss = {}))
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricDisabledWarningPreview_Light() {
TangemTheme {
BiometricDisabledWarningSample()
}
}
@Preview(showBackground = true, widthDp = 360)
@Composable
private fun BiometricDisabledWarningPreview_Dark() {
TangemTheme(isDark = true) {
BiometricDisabledWarningSample()
}
}
// endregion Preview

View file

@ -9,4 +9,9 @@ internal sealed interface WarningModel {
data class KeyInvalidatedWarning(
val onDismiss: () -> Unit,
) : WarningModel
data class BiometricsDisabledWarning(
val onConfirm: () -> Unit,
val onDismiss: () -> Unit,
) : WarningModel
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="22dp"
android:height="18dp"
android:viewportWidth="22"
android:viewportHeight="18">
<path
android:pathData="M18.636,1.924C17.212,1.259 15.689,0.775 14.097,0.5C13.902,0.853 13.673,1.327 13.516,1.705C11.824,1.451 10.147,1.451 8.486,1.705C8.328,1.327 8.095,0.853 7.897,0.5C6.304,0.775 4.779,1.26 3.355,1.927C0.483,6.26 -0.296,10.486 0.093,14.651C1.999,16.071 3.845,16.934 5.66,17.498C6.108,16.882 6.508,16.228 6.852,15.538C6.196,15.29 5.568,14.983 4.975,14.626C5.132,14.51 5.286,14.388 5.435,14.263C9.055,15.953 12.988,15.953 16.565,14.263C16.715,14.388 16.869,14.51 17.025,14.626C16.43,14.984 15.8,15.291 15.144,15.54C15.488,16.228 15.887,16.884 16.336,17.5C18.153,16.935 20.001,16.073 21.906,14.651C22.363,9.822 21.126,5.636 18.636,1.924ZM7.345,12.089C6.259,12.089 5.368,11.076 5.368,9.843C5.368,8.61 6.24,7.596 7.345,7.596C8.451,7.596 9.342,8.608 9.323,9.843C9.325,11.076 8.451,12.089 7.345,12.089ZM14.655,12.089C13.568,12.089 12.677,11.076 12.677,9.843C12.677,8.61 13.549,7.596 14.655,7.596C15.76,7.596 16.651,8.608 16.632,9.843C16.632,11.076 15.76,12.089 14.655,12.089Z"
android:fillColor="#B0B0B0"/>
</vector>

View file

@ -9,6 +9,6 @@
},
{
"name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED",
"version": "4.5.0"
"version": "undefined"
}
]

View file

@ -27,6 +27,7 @@
<string name="biometric_lockout_permanent_warning_description">Пожалуйста, отсканируйте карту</string>
<string name="biometric_lockout_warning_description">Пожалуйста, попробуйте снова через 30 секунд или отсканируйте карту</string>
<string name="biometric_lockout_warning_title">Слишком много попыток</string>
<string name="biometric_unavailable_warning">Вы отключили биометрическую аутентификацию на вашем телефоне и не сможете сохранять кошельки в приложении. Для сохранения кошельков, пожалуйста, включите функцию биометрической аутентификации в настройках телефона.</string>
<string name="card_settings_access_code_recovery_disabled_description">Отключите эту опцию, если не хотите, чтобы эта карта использовалась для сброса кодов доступа на другие карты этого кошелька. Обратите внимание, сброс кода также не будет доступен на этой карте.</string>
<string name="card_settings_access_code_recovery_enabled_description">Использовать эту карту для сброса кода доступа на других картах в этом кошельке</string>
<string name="card_settings_access_code_recovery_footer">Отключить возможность сброса кода доступа на этой карте или других картах этого кошелька</string>

View file

@ -27,6 +27,7 @@
<string name="biometric_lockout_permanent_warning_description">Please scan the card</string>
<string name="biometric_lockout_warning_description">Please try again in 30 seconds or scan the card</string>
<string name="biometric_lockout_warning_title">Too many attempts</string>
<string name="biometric_unavailable_warning">You have disabled biometric authentication on your phone and will not be able to save wallets in the app. To save wallets, please enable the biometric authentication function in your phone settings.</string>
<string name="card_settings_access_code_recovery_disabled_description">Disable this option if you don\'t want this card to be used to reset access codes on other cards of this wallet. Note that you will not be able to reset access code on this card as well.</string>
<string name="card_settings_access_code_recovery_enabled_description">Allows you to use this card to reset access code on other cards in this wallet</string>
<string name="card_settings_access_code_recovery_footer">Disable the ability to reset access code on this card or other cards in this wallet</string>

View file

@ -45,7 +45,7 @@ object SaltPayWorkaround {
) + attachTestWalletCardIds()
val walletCardIdRanges = listOf(
CardIdRange("AC05000000000003", "AC05000000023997")!!,
CardIdRange("AC05000000000003", "AC05000000015993")!!,
) + attachTestWalletCardIdRanges()
fun tokenFrom(blockchain: Blockchain): Token {