Updated on 2026-08-14
This commit is contained in:
commit
b6f4d67628
19 changed files with 179 additions and 11 deletions
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ internal class BiometricUserWalletsKeysRepository(
|
|||
UserWalletsListError.BiometricsAuthenticationLockout(isPermanent = true)
|
||||
is TangemSdkError.BiometricCryptographyKeyInvalidated ->
|
||||
UserWalletsListError.EncryptionKeyInvalidated
|
||||
is TangemSdkError.BiometricsUnavailable ->
|
||||
UserWalletsListError.BiometricsAuthenticationDisabled
|
||||
else -> error
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
9
app/src/main/res/drawable/ic_discord.xml
Normal file
9
app/src/main/res/drawable/ic_discord.xml
Normal 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>
|
||||
|
|
@ -9,6 +9,6 @@
|
|||
},
|
||||
{
|
||||
"name": "REDESIGNED_CUSTOM_TOKEN_SCREEN_ENABLED",
|
||||
"version": "4.5.0"
|
||||
"version": "undefined"
|
||||
}
|
||||
]
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ object SaltPayWorkaround {
|
|||
) + attachTestWalletCardIds()
|
||||
|
||||
val walletCardIdRanges = listOf(
|
||||
CardIdRange("AC05000000000003", "AC05000000023997")!!,
|
||||
CardIdRange("AC05000000000003", "AC05000000015993")!!,
|
||||
) + attachTestWalletCardIdRanges()
|
||||
|
||||
fun tokenFrom(blockchain: Blockchain): Token {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue