Updated on 2026-08-14

This commit is contained in:
Tangem 2022-08-11 13:34:26 +03:00
commit 00e9e964ab
28 changed files with 346 additions and 226 deletions

View file

@ -59,7 +59,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
return
}
val context = context ?: return
if (dialog != null && dialog == state.dialog) return
if (dialog != null) return
dialog = when (state.dialog) {
is AppDialog.SimpleOkDialog -> SimpleOkDialog.create(state.dialog, context)
@ -101,7 +101,7 @@ class DialogManager : StoreSubscriber<GlobalState> {
is WalletConnectDialog.ApproveWcSession ->
ApproveWcSessionDialog.create(state.dialog.session, state.dialog.networks, context)
is WalletConnectDialog.ChooseNetwork ->
ChooseNetworkDialog.create(state.dialog.networks, context)
ChooseNetworkDialog.create(state.dialog.session, state.dialog.networks, context)
is WalletConnectDialog.ClipboardOrScanQr ->
ClipboardOrScanQrDialog.create(state.dialog.clipboardUri, context)
is WalletConnectDialog.RequestTransaction ->

View file

@ -15,6 +15,7 @@ import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.configurable.warningMessage.WarningMessagesManager
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.network.exchangeServices.CardExchangeRules
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoApi
import com.tangem.tap.network.exchangeServices.mercuryo.MercuryoService
@ -23,11 +24,11 @@ import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import java.util.Locale
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
import org.rekotlin.Middleware
import java.util.*
class GlobalMiddleware {
companion object {
@ -62,9 +63,11 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
}
}
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency()
))
store.dispatch(
GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.fiatCurrenciesPrefStorage.getAppCurrency(),
),
)
}
is GlobalAction.HideWarningMessage -> {
store.state.globalState.warningManager?.let {
@ -107,7 +110,13 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
secret = mercuryoSecret,
)
val sellService = MoonPayService(moonPayKey, moonPaySecretKey)
val exchangeManager = CurrencyExchangeManager(buyService, sellService)
val cardProvider = { store.state.globalState.scanResponse?.card }
val exchangeManager = CurrencyExchangeManager(
buyService = buyService,
sellService = sellService,
primaryRules = CardExchangeRules(cardProvider),
)
store.dispatchOnMain(GlobalAction.ExchangeManager.Init.Success(exchangeManager))
store.dispatchOnMain(GlobalAction.ExchangeManager.Update)
}
@ -127,7 +136,7 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
store.state.globalState.analyticsHandlers,
currenciesRepository,
action.additionalBlockchainsToDerive,
action.messageResId
action.messageResId,
)
withMainContext {
store.dispatch(GlobalAction.ScanFailsCounter.ChooseBehavior(result))
@ -150,15 +159,15 @@ private fun handleAction(action: Action, appState: () -> AppState?, dispatch: Di
is Result.Success -> {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = result.data.code.lowercase()
)
countryCode = result.data.code.lowercase(),
),
)
}
is Result.Failure -> {
store.dispatchOnMain(
GlobalAction.FetchUserCountry.Success(
countryCode = Locale.getDefault().country.lowercase()
)
countryCode = Locale.getDefault().country.lowercase(),
),
)
}
}

View file

@ -68,16 +68,15 @@ class ScanProductTask(
when (processorResult) {
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
when (scanTaskResult) {
is CompletionResult.Success -> callback(
CompletionResult.Success(
processorResult.data
is CompletionResult.Success -> {
// it need because processorResult.data.card doesn't contains attestation result
// and CardWallet.derivedKeys
val processorScanResponseWithNewCard = processorResult.data.copy(
card = scanTaskResult.data
)
)
is CompletionResult.Failure -> callback(
CompletionResult.Failure(
scanTaskResult.error
)
)
callback(CompletionResult.Success(processorScanResponseWithNewCard))
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(scanTaskResult.error))
}
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(processorResult.error))
@ -102,11 +101,11 @@ private class ScanNoteProcessor : ProductCommandProcessor<ScanResponse> {
callback(
CompletionResult.Success(
ScanResponse(
card,
ProductType.Note,
session.environment.walletData
)
)
card = card,
productType = ProductType.Note,
walletData = session.environment.walletData,
),
),
)
}
}
@ -117,19 +116,17 @@ private class ScanWalletProcessor(
) : ProductCommandProcessor<ScanResponse> {
var primaryCard: PrimaryCard? = null
override fun proceed(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
createMissingWalletsIfNeeded(card, session, callback)
}
private fun createMissingWalletsIfNeeded(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
if (card.wallets.isEmpty() || card.firmwareVersion < FirmwareVersion.MultiWalletAvailable) {
startLinkingForBackupIfNeeded(card, session, callback)
@ -146,18 +143,12 @@ private class ScanWalletProcessor(
when (result) {
is CompletionResult.Success -> {
PreflightReadTask(
PreflightReadMode.FullCardRead,
card.cardId
readMode = PreflightReadMode.FullCardRead,
cardId = card.cardId
).run(session) { readResult ->
when (readResult) {
is CompletionResult.Success -> {
startLinkingForBackupIfNeeded(card, session, callback)
}
is CompletionResult.Failure -> callback(
CompletionResult.Failure(
readResult.error
)
)
is CompletionResult.Success -> startLinkingForBackupIfNeeded(card, session, callback)
is CompletionResult.Failure -> callback(CompletionResult.Failure(readResult.error))
}
}
}
@ -165,18 +156,14 @@ private class ScanWalletProcessor(
}
}
}
private fun startLinkingForBackupIfNeeded(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
val activationIsFinished =
preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId)
val activationIsFinished = preferencesStorage.usedCardsPrefStorage.isActivationFinished(card.cardId)
if (card.backupStatus == Card.BackupStatus.NoBackup &&
!activationIsFinished && card.wallets.isNotEmpty()
) {
if (card.backupStatus == Card.BackupStatus.NoBackup && !activationIsFinished && card.wallets.isNotEmpty()) {
StartPrimaryCardLinkingTask().run(session) { linkingResult ->
when (linkingResult) {
is CompletionResult.Success -> {
@ -192,11 +179,10 @@ private class ScanWalletProcessor(
deriveKeysIfNeeded(card, session, callback)
}
}
private fun deriveKeysIfNeeded(
card: Card,
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
scope.launch {
val derivations = collectDerivations(card)
@ -235,12 +221,13 @@ private class ScanWalletProcessor(
private suspend fun getBlockchainsToDerive(card: Card): List<BlockchainNetwork> {
val currenciesRepository = currenciesRepository ?: return emptyList()
val cardCurrencies = currenciesRepository.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
val cardCurrencies = currenciesRepository
.loadSavedCurrencies(card.cardId, card.settings.isHDWalletAllowed).toMutableList()
val blockchainsToDerive = cardCurrencies.ifEmpty {
mutableListOf(
BlockchainNetwork(Blockchain.Bitcoin, card),
BlockchainNetwork(Blockchain.Ethereum, card)
BlockchainNetwork(Blockchain.Ethereum, card),
)
}
@ -248,7 +235,7 @@ private class ScanWalletProcessor(
blockchainsToDerive.addAll(
listOf(
BlockchainNetwork(Blockchain.Ethereum, card),
BlockchainNetwork(Blockchain.EthereumTestnet, card)
BlockchainNetwork(Blockchain.EthereumTestnet, card),
)
)
}
@ -305,52 +292,44 @@ private class ScanTwinProcessor : ProductCommandProcessor<ScanResponse> {
is CompletionResult.Success -> {
val publicKey = card.getSingleWallet()?.publicKey
if (publicKey == null) {
callback(
CompletionResult.Success(
ScanResponse(
card,
ProductType.Twins,
null
)
)
)
return@run
}
val verified =
TwinsHelper.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey)
if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletData = session.environment.walletData
val response = ScanResponse(
card,
ProductType.Twins,
walletData,
twinPublicKey.toHexString()
card = card,
productType = ProductType.Twins,
walletData = null,
)
callback(CompletionResult.Success(response))
return@run
}
val verified = TwinsHelper.verifyTwinPublicKey(readDataResult.data.issuerData, publicKey)
val response = if (verified) {
val twinPublicKey = readDataResult.data.issuerData.sliceArray(0 until 65)
val walletData = session.environment.walletData
ScanResponse(
card = card,
productType = ProductType.Twins,
walletData = walletData,
secondTwinPublicKey = twinPublicKey.toHexString(),
)
} else {
callback(
CompletionResult.Success(
ScanResponse(
card,
ProductType.Twins,
null
)
)
ScanResponse(
card = card,
productType = ProductType.Twins,
walletData = null,
)
}
callback(CompletionResult.Success(response))
}
is CompletionResult.Failure ->
is CompletionResult.Failure -> {
callback(CompletionResult.Success(ScanResponse(card, ProductType.Twins, null)))
}
}
}
}
}
fun Card.getCurvesForNonCreatedWallets(): List<EllipticCurve> {
val curvesPresent = wallets.map { it.curve }.toSet()
val curvesForNonCreatedWallets = supportedCurves
.subtract(curvesPresent + EllipticCurve.Secp256r1)
val curvesForNonCreatedWallets = supportedCurves.subtract(curvesPresent + EllipticCurve.Secp256r1)
return curvesForNonCreatedWallets.toList()
}

View file

@ -14,6 +14,7 @@ import com.tangem.common.CompletionResult
import com.tangem.domain.common.ScanResponse
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.onboarding.products.wallet.redux.BackupAction
import com.tangem.tap.features.wallet.redux.WalletAction
@ -42,7 +43,8 @@ object DemoHelper {
WalletAction.TradeCryptoAction.Buy::class.java,
WalletAction.TradeCryptoAction.Sell::class.java,
BackupAction.StartBackup::class.java,
WalletAction.ExploreAddress::class.java
WalletAction.ExploreAddress::class.java,
DetailsAction.ResetToFactory.Start::class.java,
)
fun isDemoCard(scanResponse: ScanResponse): Boolean = isDemoCardId(scanResponse.card.cardId)

View file

@ -19,6 +19,7 @@ sealed class DetailsAction : Action {
object ReCreateTwinsWallet : DetailsAction()
sealed class ResetToFactory : DetailsAction() {
object Start : ResetToFactory()
object Proceed : ResetToFactory()
data class Confirm(val confirmed: Boolean) : ResetToFactory()
object Failure : ResetToFactory()

View file

@ -12,7 +12,7 @@ import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.currenciesRepository
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.demo.DemoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.CreateTwinWalletMode
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsAction
import com.tangem.tap.features.wallet.models.hasSendableAmountsOrPendingTransactions
@ -22,70 +22,74 @@ import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Action
import org.rekotlin.Middleware
class DetailsMiddleware {
private val eraseWalletMiddleware = EraseWalletMiddleware()
private val manageSecurityMiddleware = ManageSecurityMiddleware()
private val managePrivacyMiddleware = ManagePrivacyMiddleware()
val detailsMiddleware: Middleware<AppState> = { _, _ ->
val detailsMiddleware: Middleware<AppState> = { _, state ->
{ next ->
{ action ->
when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
val uri = store.state.detailsState.cardTermsOfUseUrl
if (uri != null) {
store.dispatch(NavigationAction.OpenDocument(uri))
} else {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}
handleAction(state, action)
next(action)
}
}
}
private fun handleAction(state: () -> AppState?, action: Action) {
if (DemoHelper.tryHandle(state, action)) return
when (action) {
is DetailsAction.ResetToFactory -> eraseWalletMiddleware.handle(action)
is DetailsAction.ManageSecurity -> manageSecurityMiddleware.handle(action)
is DetailsAction.AppSettings -> managePrivacyMiddleware.handle(action)
is DetailsAction.ShowDisclaimer -> {
val uri = store.state.detailsState.cardTermsOfUseUrl
if (uri != null) {
store.dispatch(NavigationAction.OpenDocument(uri))
}
}
is DetailsAction.ReCreateTwinsWallet -> {
val wallet =
store.state.walletState.walletManagers.map { it.wallet }.firstOrNull()
if (wallet == null) {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
} else {
if (wallet.hasSendableAmountsOrPendingTransactions()) {
val walletIsNotEmpty =
store.state.globalState.resources.strings.walletIsNotEmpty
store.dispatchNotification(walletIsNotEmpty)
} else {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
is DetailsAction.ReCreateTwinsWallet -> {
val wallet =
store.state.walletState.walletManagers.map { it.wallet }.firstOrNull()
if (wallet == null) {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
} else {
if (wallet.hasSendableAmountsOrPendingTransactions()) {
val walletIsNotEmpty =
store.state.globalState.resources.strings.walletIsNotEmpty
store.dispatchNotification(walletIsNotEmpty)
} else {
store.dispatch(TwinCardsAction.SetMode(CreateTwinWalletMode.RecreateWallet))
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingTwins))
}
}
}
is DetailsAction.CreateBackup -> {
store.state.detailsState.scanResponse?.let {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatch(
GlobalAction.Onboarding.Start(
it,
fromHomeScreen = false,
),
)
}
}
DetailsAction.ScanCard -> {
scope.launch {
when (val result = tangemSdkManager.scanCard()) {
is CompletionResult.Success -> {
val card = result.data
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
}
}
is DetailsAction.CreateBackup -> {
store.state.detailsState.scanResponse?.let {
store.dispatch(NavigationAction.NavigateTo(AppScreen.OnboardingWallet))
store.dispatch(
GlobalAction.Onboarding.Start(
it,
fromHomeScreen = false,
),
)
}
}
DetailsAction.ScanCard -> {
scope.launch {
when (val result = tangemSdkManager.scanCard()) {
is CompletionResult.Success -> {
val card = result.data
store.dispatchOnMain(DetailsAction.PrepareCardSettingsData(card))
}
is CompletionResult.Failure -> {
}
}
is CompletionResult.Failure -> {
}
}
}
next(action)
}
}
}
@ -93,6 +97,9 @@ class DetailsMiddleware {
class EraseWalletMiddleware {
fun handle(action: DetailsAction.ResetToFactory) {
when (action) {
is DetailsAction.ResetToFactory.Start -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
}
is DetailsAction.ResetToFactory.Proceed -> {
val card = store.state.detailsState.cardSettingsState?.card ?: return
if (card.isTangemTwins()) {

View file

@ -47,7 +47,7 @@ sealed class WalletConnectAction : Action {
val session: WalletConnectSession,
) : WalletConnectAction()
data class SelectNetwork(val networks: List<Blockchain>) : WalletConnectAction()
data class SelectNetwork(val session: WalletConnectSession, val networks: List<Blockchain>) : WalletConnectAction()
data class ChooseNetwork(val blockchain: Blockchain) : WalletConnectAction()
data class UpdateBlockchain(
val updatedSession: WalletConnectSession,

View file

@ -62,7 +62,14 @@ class WalletConnectMiddleware {
}
}
is WalletConnectAction.SelectNetwork -> {
store.dispatch(GlobalAction.ShowDialog(WalletConnectDialog.ChooseNetwork(action.networks)))
store.dispatch(
GlobalAction.ShowDialog(
WalletConnectDialog.ChooseNetwork(
session = action.session,
networks = action.networks,
),
),
)
}
is WalletConnectAction.ChooseNetwork -> {
val data = state()?.walletConnectState?.newSessionData ?: return

View file

@ -95,10 +95,12 @@ sealed class WalletConnectDialog : StateDialog {
object OpeningSessionRejected : WalletConnectDialog()
object SessionTimeout : WalletConnectDialog()
data class ApproveWcSession(
val session: WalletConnectSession, val networks: List<Blockchain>,
val session: WalletConnectSession,
val networks: List<Blockchain>,
) : WalletConnectDialog()
data class ChooseNetwork(
val session: WalletConnectSession,
val networks: List<Blockchain>,
) : WalletConnectDialog()

View file

@ -3,8 +3,6 @@ package com.tangem.tap.features.details.ui.cardsettings
import com.tangem.domain.common.getTwinCardIdForUser
import com.tangem.domain.common.isTangemTwins
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.CardSettingsState
import com.tangem.tap.features.details.redux.DetailsAction
import org.rekotlin.Store
@ -64,7 +62,7 @@ class CardSettingsViewModel(private val store: Store<AppState>) {
store.dispatch(DetailsAction.ManageSecurity.ChangeAccessCode)
}
is CardInfo.ResetToFactorySettings -> {
store.dispatch(NavigationAction.NavigateTo(AppScreen.ResetToFactory))
store.dispatch(DetailsAction.ResetToFactory.Start)
}
is CardInfo.SecurityMode -> {
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)

View file

@ -20,6 +20,7 @@ import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -32,42 +33,61 @@ fun SettingsScreensScaffold(
content: @Composable () -> Unit,
background: @Composable (() -> Unit)? = null,
fab: @Composable (() -> Unit)? = null,
titleRes: Int,
backgroundColor: Color = colorResource(id = R.color.background_primary),
titleRes: Int? = null,
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
) {
BackHandler(true, onBackClick)
Scaffold(
topBar = { EmptyTopBarWithNavigation(onBackClick = onBackClick) },
topBar = {
EmptyTopBarWithNavigation(
onBackClick = onBackClick,
backgroundColor = backgroundColor,
)
},
modifier = modifier.systemBarsPadding(),
backgroundColor = colorResource(id = R.color.background_primary),
backgroundColor = backgroundColor,
floatingActionButton = { fab?.invoke() },
) {
if (titleRes != null) {
Box(modifier = modifier.fillMaxSize()) {
background?.invoke()
Box(modifier = modifier.fillMaxSize()) {
background?.invoke()
Column(
modifier = modifier.fillMaxWidth(),
) {
Text(
text = stringResource(id = titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp, bottom = 52.dp),
style = TangemTypography.headline1,
color = colorResource(id = R.color.text_primary_1),
)
content()
Column(modifier = modifier.fillMaxWidth()) {
Text(
text = stringResource(id = titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp, bottom = 52.dp),
style = TangemTypography.headline1,
color = colorResource(id = R.color.text_primary_1),
)
content()
}
}
} else {
content()
}
}
}
@Composable
fun ScreenTitle(
titleRes: Int,
modifier: Modifier = Modifier,
) {
Text(
text = stringResource(id = titleRes),
modifier = modifier.padding(start = 20.dp, end = 20.dp),
style = TangemTypography.headline1,
color = colorResource(id = R.color.text_primary_1),
)
}
@Composable
fun EmptyTopBarWithNavigation(
onBackClick: () -> Unit,
backgroundColor: Color = colorResource(id = R.color.background_primary),
modifier: Modifier = Modifier,
) {
TopAppBar(
@ -81,7 +101,7 @@ fun EmptyTopBarWithNavigation(
)
}
},
backgroundColor = colorResource(id = R.color.background_primary),
backgroundColor = backgroundColor,
elevation = 0.dp,
)
}

View file

@ -10,9 +10,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@ -24,6 +25,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -34,10 +36,7 @@ fun DetailsScreen(
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content = {
Content(state = state, modifier = modifier)
},
titleRes = R.string.details_title,
content = { Content(state = state, modifier = modifier) },
onBackClick = onBackPressed,
)
}
@ -50,37 +49,32 @@ fun Content(
Column(
modifier = modifier
.fillMaxSize()
.padding(bottom = 40.dp),
.verticalScroll(rememberScrollState()),
) {
LazyColumn(
modifier = modifier
.fillMaxWidth()
.padding(bottom = 40.dp)
.weight(1f),
) {
items(state.elements) {
if (it == SettingsElement.WalletConnect) {
WalletConnectDetailsItem(
onItemsClick = state.onItemsClick,
modifier = modifier,
)
} else {
DetailsItem(
item = it,
appCurrency = state.appCurrency,
onItemsClick = state.onItemsClick,
modifier = modifier,
)
}
ScreenTitle(titleRes = R.string.details_title, modifier.padding(bottom = 52.dp))
state.elements.map {
if (it == SettingsElement.WalletConnect) {
WalletConnectDetailsItem(
onItemsClick = state.onItemsClick,
modifier = modifier,
)
} else {
DetailsItem(
item = it,
appCurrency = state.appCurrency,
onItemsClick = state.onItemsClick,
modifier = modifier,
)
}
}
Spacer(modifier = modifier.weight(1f))
TangemSocialAccounts(state.tangemLinks, state.onSocialNetworkClick)
Spacer(modifier = Modifier.size(12.dp))
Text(
text = "${stringResource(id = state.appNameRes)} ${state.tangemVersion}",
style = TangemTypography.caption,
color = colorResource(id = R.color.text_tertiary),
modifier = modifier.padding(start = 16.dp, end = 16.dp),
modifier = modifier.padding(start = 16.dp, end = 16.dp, bottom = 40.dp),
)
}
}

View file

@ -24,7 +24,8 @@ enum class SettingsElement(
AppCurrency(R.drawable.ic_currency, R.string.details_row_title_currency),
AppSettings(R.drawable.ic_settings, R.string.app_settings_title),
LinkMoreCards(R.drawable.ic_more_cards, R.string.details_row_title_create_backup),
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title),
TermsOfService(R.drawable.ic_text, R.string.disclaimer_title), // General Terms of Service of the App
TermsOfUse(R.drawable.ic_text, R.string.details_row_title_card_tou), // Terms of Use for S2C cards only
PrivacyPolicy(R.drawable.ic_lock, R.string.details_row_privacy_policy),
;
}

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.details.ui.details
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.tap.common.feedback.FeedbackEmail
import com.tangem.tap.common.feedback.SupportInfo
import com.tangem.tap.common.redux.AppState
@ -9,6 +10,7 @@ import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.domain.extensions.isMultiwalletAllowed
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.disclaimer.redux.DisclaimerAction
import com.tangem.tap.features.home.LocaleRegionProvider
import com.tangem.tap.features.home.RUSSIA_COUNTRY_CODE
import com.tangem.tap.features.wallet.redux.WalletAction
@ -30,6 +32,7 @@ class DetailsViewModel(private val store: Store<AppState>) {
}
SettingsElement.AppSettings -> null // TODO: until we implement settings from this screen
SettingsElement.AppCurrency -> if (state.scanResponse?.card?.isMultiwalletAllowed != true) it else null
SettingsElement.TermsOfUse -> if (state.scanResponse?.card?.isStart2Coin == true) it else null
else -> it
}
}
@ -72,6 +75,10 @@ class DetailsViewModel(private val store: Store<AppState>) {
store.dispatch(DetailsAction.CreateBackup)
}
SettingsElement.TermsOfService -> {
store.dispatch(DisclaimerAction.ShowAcceptedDisclaimer)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Disclaimer))
}
SettingsElement.TermsOfUse -> {
store.dispatch(DetailsAction.ShowDisclaimer)
}
SettingsElement.PrivacyPolicy -> {

View file

@ -1,13 +1,16 @@
package com.tangem.tap.features.details.ui.resetcard
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.Icon
@ -15,6 +18,7 @@ import androidx.compose.material.IconToggleButton
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -22,6 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.tap.common.compose.TangemTypography
import com.tangem.tap.features.details.ui.common.DetailsMainButton
import com.tangem.tap.features.details.ui.common.ScreenTitle
import com.tangem.tap.features.details.ui.common.SettingsScreensScaffold
import com.tangem.wallet.R
@ -31,14 +36,18 @@ fun ResetCardScreen(
onBackPressed: () -> Unit,
modifier: Modifier = Modifier,
) {
SettingsScreensScaffold(
content = { ResetCardView(state = state, modifier = modifier) },
background =
{ Image(painter = painterResource(id = R.drawable.ic_reset_background), contentDescription = "") },
titleRes = R.string.reset_card_to_factory_navigation_title,
onBackClick = onBackPressed,
)
Box(modifier = modifier.background(colorResource(id = R.color.background_primary))) {
Image(
painter = painterResource(id = R.drawable.ic_reset_background),
contentDescription = "",
modifier = modifier.offset(y = (-16).dp),
)
SettingsScreensScaffold(
content = { ResetCardView(state = state, modifier = modifier) },
onBackClick = onBackPressed,
backgroundColor = Color.Transparent,
)
}
}
@Composable
@ -51,7 +60,16 @@ fun ResetCardView(
.fillMaxSize(),
verticalArrangement = Arrangement.Bottom,
) {
Box(
modifier = modifier,
) {
ScreenTitle(titleRes = R.string.reset_card_to_factory_navigation_title)
}
Spacer(
modifier = modifier
.defaultMinSize(20.dp)
.weight(1f),
)
Text(
text = stringResource(id = R.string.common_attention),
modifier = modifier.padding(start = 20.dp, end = 20.dp),

View file

@ -6,10 +6,12 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.selectable
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@ -45,7 +47,8 @@ fun SecurityModeOptions(
Column(
modifier = modifier
.fillMaxSize()
.padding(bottom = 28.dp),
.padding(bottom = 28.dp)
.offset(y = (-16).dp),
verticalArrangement = Arrangement.SpaceBetween,
) {
state.availableOptions.map {
@ -85,12 +88,16 @@ fun SecurityOption(
.selectable(
selected = selected, onClick = { state.onNewModeSelected(option) },
)
.padding(start = 20.dp, end = 20.dp, bottom = 32.dp),
.padding(start = 20.dp, end = 20.dp, top = 16.dp, bottom = 16.dp),
) {
RadioButton(
selected = selected, onClick = null,
modifier = modifier.padding(end = 20.dp),
colors = RadioButtonDefaults.colors(
unselectedColor = colorResource(id = R.color.icon_secondary),
selectedColor = colorResource(id = R.color.icon_accent),
),
)
Column {

View file

@ -52,11 +52,9 @@ fun WalletConnectScreen(
}
},
fab = {
AddSessionFab(
onAddSession = {
state.onAddSession(context.getFromClipboard()?.toString())
},
)
if (!state.isLoading) {
AddSessionFab(onAddSession = { state.onAddSession(context.getFromClipboard()?.toString()) })
}
},
titleRes = R.string.wallet_connect_title,
onBackClick = onBackPressed,

View file

@ -27,7 +27,8 @@ class ApproveWcSessionDialog {
}
if (networks.size > 1) {
setNeutralButton(context.getText(R.string.wallet_connect_select_network)) { _, _ ->
store.dispatch(WalletConnectAction.SelectNetwork(networks))
store.dispatch(GlobalAction.HideDialog(WalletConnectDialog.ApproveWcSession(session, networks)))
store.dispatch(WalletConnectAction.SelectNetwork(session = session, networks = networks))
}
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->

View file

@ -5,22 +5,26 @@ import androidx.appcompat.app.AlertDialog
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectAction
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.store
import com.tangem.wallet.R
object ChooseNetworkDialog {
fun create(
blockchains: List<Blockchain>,
session: WalletConnectSession,
networks: List<Blockchain>,
context: Context,
): AlertDialog {
return AlertDialog.Builder(context)
.setTitle(context.getString(R.string.wallet_connect_select_network))
.setNegativeButton(context.getString(R.string.common_cancel)) { _, _ -> /* no-op */ }
.setNegativeButton(context.getString(R.string.common_cancel)) { _, _ ->
store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session))
}
.setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
}
.setSingleChoiceItems(blockchains.map { it.fullName }.toTypedArray(), 0) { _, which ->
blockchains.getOrNull(which)?.let { selectedBlockchain ->
.setSingleChoiceItems(networks.map { it.fullName }.toTypedArray(), 0) { _, which ->
networks.getOrNull(which)?.let { selectedBlockchain ->
store.dispatch(
WalletConnectAction.ChooseNetwork(
blockchain = selectedBlockchain,

View file

@ -219,8 +219,10 @@ private fun sendTransaction(
return@launch
}
withContext(Dispatchers.Main) {
withMainContext {
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
tangemSdk.config.linkedTerminal = isLinkedTerminal
when (sendResult) {
is SimpleResult.Success -> {
store.state.globalState.analyticsHandlers?.triggerEvent(
@ -260,12 +262,12 @@ private fun sendTransaction(
card = card,
)
val error = (sendResult.error as? BlockchainSdkError) ?: return@withContext
val error = (sendResult.error as? BlockchainSdkError) ?: return@withMainContext
when (error) {
is BlockchainSdkError.WrappedTangemError -> {
val tangemSdkError = (error.tangemError as? TangemSdkError) ?: return@withContext
if (tangemSdkError is TangemSdkError.UserCancelled) return@withContext
val tangemSdkError = (error.tangemError as? TangemSdkError) ?: return@withMainContext
if (tangemSdkError is TangemSdkError.UserCancelled) return@withMainContext
dispatch(SendAction.Dialog.SendTransactionFails.CardSdkError(tangemSdkError))
}
@ -294,7 +296,6 @@ private fun sendTransaction(
}
}
}
dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED))
}
}
}

View file

@ -0,0 +1,54 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.common.card.Card
import com.tangem.domain.common.TapWorkarounds.isStart2Coin
import com.tangem.tap.features.demo.isDemoCard
import com.tangem.tap.features.wallet.models.Currency
/**
[REDACTED_AUTHOR]
*/
class CardExchangeRules(
val cardProvider: () -> Card?,
) : ExchangeRules {
override fun isBuyAllowed(): Boolean {
val card = cardProvider() ?: return false
return when {
card.isDemoCard() -> false
card.isStart2Coin -> false
else -> true
}
}
override fun isSellAllowed(): Boolean {
val card = cardProvider() ?: return false
return when {
card.isDemoCard() -> false
card.isStart2Coin -> false
else -> true
}
}
override fun availableForBuy(currency: Currency): Boolean {
val card = cardProvider() ?: return false
return when {
card.isDemoCard() -> false
card.isStart2Coin -> false
else -> true
}
}
override fun availableForSell(currency: Currency): Boolean {
val card = cardProvider() ?: return false
return when {
card.isDemoCard() -> false
card.isStart2Coin -> false
else -> true
}
}
}

View file

@ -23,6 +23,7 @@ import java.math.BigDecimal
class CurrencyExchangeManager(
private val buyService: ExchangeService,
private val sellService: ExchangeService,
private val primaryRules: ExchangeRules,
) : ExchangeService, ExchangeUrlBuilder {
override suspend fun update() {
@ -30,10 +31,16 @@ class CurrencyExchangeManager(
sellService.update()
}
override fun isBuyAllowed(): Boolean = buyService.isBuyAllowed()
override fun isSellAllowed(): Boolean = sellService.isSellAllowed()
override fun availableForBuy(currency: Currency): Boolean = buyService.availableForBuy(currency)
override fun availableForSell(currency: Currency): Boolean = sellService.availableForSell(currency)
override fun isBuyAllowed(): Boolean = primaryRules.isBuyAllowed() && buyService.isBuyAllowed()
override fun isSellAllowed(): Boolean = primaryRules.isSellAllowed() && sellService.isSellAllowed()
override fun availableForBuy(currency: Currency): Boolean {
return primaryRules.availableForBuy(currency) && buyService.availableForBuy(currency)
}
override fun availableForSell(currency: Currency): Boolean {
return primaryRules.availableForSell(currency) && sellService.availableForSell(currency)
}
override fun getUrl(
action: Action,

View file

@ -3,8 +3,11 @@ package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.wallet.models.Currency
interface ExchangeService {
interface ExchangeService: ExchangeRules {
suspend fun update()
}
interface ExchangeRules {
fun isBuyAllowed(): Boolean
fun isSellAllowed(): Boolean
fun availableForBuy(currency: Currency):Boolean

View file

@ -54,7 +54,7 @@
<string name="token_details_hide_token">Hide token</string>
<string name="token_details_hide_alert_title">Hide %s</string>
<string name="token_details_hide_alert_hide">Hide</string>
<string name="token_details_hide_alert_message">You hide the token from the main screen, but you can add it back at any time.</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>

View file

@ -54,7 +54,7 @@
<string name="token_details_hide_token">Hide token</string>
<string name="token_details_hide_alert_title">Hide %s</string>
<string name="token_details_hide_alert_hide">Hide</string>
<string name="token_details_hide_alert_message">You hide the token from the main screen, but you can add it back at any time.</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>

View file

@ -54,7 +54,7 @@
<string name="token_details_hide_token">Hide token</string>
<string name="token_details_hide_alert_title">Hide %s</string>
<string name="token_details_hide_alert_hide">Hide</string>
<string name="token_details_hide_alert_message">You hide the token from the main screen, but you can add it back at any time.</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>

View file

@ -59,7 +59,7 @@
<string name="token_details_hide_token">Скрыть токен</string>
<string name="token_details_hide_alert_title">Скрыть %s</string>
<string name="token_details_hide_alert_hide">Скрыть</string>
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно.</string>
<string name="token_details_hide_alert_message">Вы скрываете токен с главного экрана, но в любой момент сможете добавить его обратно через страницу управления токенами.</string>
<string name="token_details_unable_hide_alert_title">Невозможно скрыть %s</string>
<string name="token_details_unable_hide_alert_message">Токен %s является основной валютой в сети %s и не может быть скрыт до тех пор, пока у вас в списке есть другие токены этой сети.</string>

View file

@ -59,7 +59,7 @@
<string name="token_details_hide_token">Hide token</string>
<string name="token_details_hide_alert_title">Hide %s</string>
<string name="token_details_hide_alert_hide">Hide</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime.</string>
<string name="token_details_hide_alert_message">You are about to hide this token from the main screen. You can add it back anytime through the manage tokens page.</string>
<string name="token_details_unable_hide_alert_title">Unable to hide %s</string>
<string name="token_details_unable_hide_alert_message">The %s token is the main currency on the %s network and cannot be hidden as long as you have other tokens on this network in the list.</string>