Updated on 2026-08-14

This commit is contained in:
Tangem 2022-08-23 13:55:33 +03:00
commit 86eef602fd
69 changed files with 715 additions and 570 deletions

View file

@ -18,12 +18,22 @@ class NavBarInsetsFragmentLifecycleCallback : FragmentLifecycleCallbacks() {
savedInstanceState: Bundle?,
) {
if (v is ComposeView) return
ViewCompat.setOnApplyWindowInsetsListener(v) { view, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
view.updatePadding(
bottom = insets.bottom,
)
val statusBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
val navigationBarInsets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
if (view.fitsSystemWindows) {
view.updatePadding(
top = statusBarInsets.top,
bottom = navigationBarInsets.bottom,
)
} else {
view.updatePadding(
bottom = navigationBarInsets.bottom,
)
}
windowInsets
}
}
}
}

View file

@ -122,11 +122,19 @@ class TapApplication : Application(), ImageLoaderFactory {
)
val logWriter = TangemLogCollector(
levels = logLevels,
messageFormatter = LogFormat.StairsFormatter()
messageFormatter = LogFormat.StairsFormatter(),
)
Log.addLogger(logWriter)
store.dispatch(GlobalAction.SetFeedbackManager(FeedbackManager(infoHolder, logWriter)))
store.dispatch(
GlobalAction.SetFeedbackManager(
FeedbackManager(
infoHolder = infoHolder,
logCollector = logWriter,
preferencesStorage = preferencesStorage,
),
),
)
}
private fun initAppsFlyer() {

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

@ -116,6 +116,8 @@ fun BigDecimal.formatAmountAsSpannedString(
val integer = amount.substringBefore('.')
val reminder = amount.substringAfter('.')
// test formatter log Log.e("TEST ", BigDecimal("1234567890987654321.1234567890987654321").formatWithSpaces())
return buildSpannedString {
append(integer)
append('.')
@ -136,7 +138,7 @@ fun BigDecimal.formatWithSpaces(): String {
var index: Int = integerStr.length
while (0 < index) {
if (index <= 3) {
packets.add(0, integerStr)
packets.add(integerStr)
break
}
index -= 3
@ -145,7 +147,7 @@ fun BigDecimal.formatWithSpaces(): String {
}
return buildString {
packets.forEachIndexed { index, packet ->
packets.reversed().forEachIndexed { index, packet ->
append(packet)
if (index != packets.lastIndex) append(' ')
}

View file

@ -76,6 +76,6 @@ fun Store<*>.dispatchDialogShow(dialog: StateDialog) {
fun Store<*>.dispatchDialogHide() {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
}

View file

@ -22,9 +22,8 @@ import timber.log.Timber
*/
suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
val scanResponse = store.state.globalState.scanResponse
?: error("Scan response must not be null")
if (scanResponse.isDemoCard() || TestActions.testAmountInjectionForWalletManagerEnabled) {
if (scanResponse?.isDemoCard() == true || TestActions.testAmountInjectionForWalletManagerEnabled) {
delay(500)
TestActions.testAmountInjectionForWalletManagerEnabled = false
Result.Success(wallet)
@ -52,11 +51,10 @@ suspend fun WalletManager.safeUpdate(): Result<Wallet> = try {
fun WalletManager?.getToUpUrl(): String? {
val globalState = store.state.globalState
val exchangeManager = globalState.exchangeManager ?: return null
val wallet = this?.wallet ?: return null
val defaultAddress = wallet.address
return exchangeManager.getUrl(
return globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Buy,
blockchain = wallet.blockchain,
cryptoCurrencyName = wallet.blockchain.currency,

View file

@ -0,0 +1,8 @@
package com.tangem.tap.common.feature
/**
[REDACTED_AUTHOR]
*/
interface Feature {
fun featureIsSwitchedOn():Boolean
}

View file

@ -1,12 +1,14 @@
package com.tangem.tap.common.feedback
import android.content.Context
import android.os.Build
import com.tangem.domain.common.TapWorkarounds
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.log.TangemLogCollector
import com.tangem.tap.common.zendesk.ZendeskConfig
import com.tangem.tap.foregroundActivityObserver
import com.tangem.tap.logConfig
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.tap.withForegroundActivity
import com.tangem.wallet.R
import com.zendesk.logger.Logger
@ -14,6 +16,8 @@ import timber.log.Timber
import zendesk.chat.Chat
import zendesk.chat.ChatConfiguration
import zendesk.chat.ChatEngine
import zendesk.chat.ChatProvidersConfiguration
import zendesk.chat.VisitorInfo
import zendesk.configurations.Configuration
import zendesk.messaging.MessagingActivity
import java.io.File
@ -26,6 +30,7 @@ import java.io.StringWriter
class FeedbackManager(
val infoHolder: AdditionalFeedbackInfo,
private val logCollector: TangemLogCollector,
private val preferencesStorage: PreferencesStorage,
) {
fun initChat(
context: Context,
@ -58,6 +63,7 @@ class FeedbackManager(
fun openChat(feedbackData: FeedbackData) {
feedbackData.prepare(infoHolder)
foregroundActivityObserver.withForegroundActivity { activity ->
setChatVisitorInfo()
setChatVisitorNote(activity, feedbackData)
showMessagingActivity(activity)
}
@ -82,6 +88,20 @@ class FeedbackManager(
}
}
private fun setChatVisitorInfo() {
if (preferencesStorage.chatFirstLaunchTime == null) {
preferencesStorage.chatFirstLaunchTime = System.currentTimeMillis()
}
val chatUserId = (preferencesStorage.chatFirstLaunchTime.toString() + Build.MODEL).hashCode()
val visitorInfo = VisitorInfo.builder()
.withName("User $chatUserId")
.build()
Chat.INSTANCE.chatProvidersConfiguration = ChatProvidersConfiguration.builder()
.withVisitorInfo(visitorInfo)
.build()
}
private fun setChatVisitorNote(
context: Context,
feedbackData: FeedbackData,

View file

@ -34,7 +34,7 @@ sealed class GlobalAction : Action {
// dialogs
data class ShowDialog(val stateDialog: StateDialog) : GlobalAction()
data class HideDialog(val stateDialog: StateDialog? = null) : GlobalAction()
object HideDialog : GlobalAction()
sealed class Onboarding {
data class Start(val scanResponse: ScanResponse?, val fromHomeScreen: Boolean = true) : GlobalAction()

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,11 +68,7 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
globalState.copy(dialog = action.stateDialog)
}
is GlobalAction.HideDialog -> {
if (action.stateDialog == null || action.stateDialog == globalState.dialog) {
globalState.copy(dialog = null)
} else {
globalState
}
globalState.copy(dialog = null)
}
is GlobalAction.ExchangeManager.Init.Success -> {
globalState.copy(exchangeManager = action.exchangeManager)

View file

@ -25,7 +25,7 @@ data class GlobalState(
val appCurrency: FiatCurrency = FiatCurrency.Default,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val exchangeManager: CurrencyExchangeManager? = null,
val exchangeManager: CurrencyExchangeManager = CurrencyExchangeManager.dummy(),
val resources: AndroidResources = AndroidResources(),
val analyticsHandlers: AnalyticsHandler? = null,
val userCountryCode: String? = null,

View file

@ -51,7 +51,7 @@ class SimpleCancelableAlertDialog {
setNegativeButton(context.getText(secondaryButtonRes)) { _, _ -> secondaryButtonAction()}
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}

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

@ -7,13 +7,14 @@ import com.tangem.blockchain.common.TransactionData
import com.tangem.blockchain.common.TransactionSender
import com.tangem.blockchain.common.TransactionSigner
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.toBlockchainCustomError
import com.tangem.blockchain.common.toBlockchainSdkError
import com.tangem.blockchain.extensions.Result
import com.tangem.blockchain.extensions.SimpleResult
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)
@ -482,7 +484,7 @@ class DemoTransactionSender(
publicKey = walletManager.wallet.publicKey
)
return when (signerResponse) {
is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainCustomError())
is CompletionResult.Success -> SimpleResult.Failure(Exception(ID).toBlockchainSdkError())
is CompletionResult.Failure -> SimpleResult.fromTangemSdkError(signerResponse.error)
}
}

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

@ -5,7 +5,6 @@ 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.WalletConnectDialog
import com.tangem.tap.features.details.redux.walletconnect.WalletConnectSession
import com.tangem.tap.store
import com.tangem.wallet.R
@ -23,18 +22,22 @@ class ApproveWcSessionDialog {
setTitle(context.getString(R.string.wallet_connect))
setMessage(message)
setPositiveButton(context.getText(R.string.common_start)) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
store.dispatch(WalletConnectAction.ChooseNetwork(session.wallet.blockchain!!))
}
if (networks.size > 1) {
setNeutralButton(context.getText(R.string.wallet_connect_select_network)) { _, _ ->
store.dispatch(WalletConnectAction.SelectNetwork(networks))
store.dispatch(GlobalAction.HideDialog)
store.dispatch(WalletConnectAction.SelectNetwork(session = session, networks = networks))
}
}
setNegativeButton(context.getText(R.string.common_reject)) { _, _ ->
store.dispatch(GlobalAction.HideDialog)
store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog(WalletConnectDialog.ApproveWcSession(session, networks)))
setOnCancelListener {
store.dispatch(GlobalAction.HideDialog)
store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session))
}
}.create()
}

View file

@ -58,7 +58,7 @@ class BnbTransactionDialog {
store.dispatch(WalletConnectAction.RejectRequest(session, sessionId))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}

View file

@ -5,28 +5,32 @@ 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 */ }
.setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
.setNegativeButton(context.getString(R.string.common_cancel)) { _, _ ->
store.dispatch(WalletConnectAction.FailureEstablishingSession(session.session))
}
.setSingleChoiceItems(blockchains.map { it.fullName }.toTypedArray(), 0) { _, which ->
blockchains.getOrNull(which)?.let { selectedBlockchain ->
.setOnDismissListener {
store.dispatch(GlobalAction.HideDialog)
}
.setSingleChoiceItems(networks.map { it.fullName }.toTypedArray(), 0) { _, which ->
networks.getOrNull(which)?.let { selectedBlockchain ->
store.dispatch(
WalletConnectAction.ChooseNetwork(
blockchain = selectedBlockchain,
),
)
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
}
.create()

View file

@ -22,7 +22,7 @@ class ClipboardOrScanQrDialog {
store.dispatch(NavigationAction.NavigateTo(AppScreen.QrScan))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}

View file

@ -29,7 +29,7 @@ class PersonalSignDialog {
store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}

View file

@ -45,7 +45,7 @@ class TransactionDialog {
store.dispatch(WalletConnectAction.RejectRequest(data.session, data.id))
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}

View file

@ -26,7 +26,7 @@ data class OnboardingNoteState(
get() = steps.indexOf(currentStep)
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { _, _ ->
store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false
store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency)
}
}

View file

@ -6,13 +6,19 @@ import com.tangem.common.extensions.guard
import com.tangem.domain.common.ScanResponse
import com.tangem.domain.common.extensions.withMainContext
import com.tangem.tap.DELAY_SDK_DIALOG_CLOSE
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.dispatchDialogShow
import com.tangem.tap.common.extensions.dispatchErrorNotification
import com.tangem.tap.common.extensions.dispatchOpenUrl
import com.tangem.tap.common.extensions.getAddressData
import com.tangem.tap.common.extensions.getToUpUrl
import com.tangem.tap.common.extensions.onCardScanned
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.common.redux.AppState
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.domain.TapError
import com.tangem.tap.domain.extensions.makePrimaryWalletManager
import com.tangem.tap.domain.twins.TwinCardsManager
@ -265,7 +271,8 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(NavigationAction.PopBackTo())
currenciesRepository.removeCurrencies(scanResponse.card.cardId)
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
}
}

View file

@ -56,7 +56,7 @@ data class TwinCardsState(
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { _, _ ->
store.state.globalState.exchangeManager?.availableForBuy(walletBalance.currency) ?: false
store.state.globalState.exchangeManager.availableForBuy(walletBalance.currency)
}
}

View file

@ -20,7 +20,7 @@ class AddMoreBackupCardsDialog {
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}

View file

@ -13,10 +13,10 @@ class BackupInProgressDialog {
setTitle(R.string.alert_title)
setMessage(R.string.onboarding_backup_exit_warning)
setPositiveButton(R.string.warning_button_ok) { _, _ ->
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
}.create()
}

View file

@ -20,7 +20,7 @@ class ConfirmDiscardingBackupDialog {
store.dispatch(BackupAction.DiscardSavedBackup)
}
setOnDismissListener {
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
}
setCancelable(false)
}.create()

View file

@ -15,11 +15,11 @@ class UnfinishedBackupFoundDialog {
setTitle(R.string.alert_title)
setMessage(R.string.welcome_interrupted_backup_alert_message)
setPositiveButton(R.string.welcome_interrupted_backup_alert_resume) { _, _ ->
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
store.dispatch(BackupAction.ResumeBackup)
}
setNegativeButton(R.string.welcome_interrupted_backup_alert_discard) { _, _ ->
store.dispatch(GlobalAction.HideDialog())
store.dispatch(GlobalAction.HideDialog)
store.dispatch(GlobalAction.ShowDialog(BackupDialog.ConfirmDiscardingBackup))
}
setCancelable(false)

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

@ -3,7 +3,18 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.IndeterminateProgressButton
import com.tangem.tap.features.send.redux.*
import com.tangem.tap.features.send.redux.AddressPayIdActionUi
import com.tangem.tap.features.send.redux.AddressPayIdVerifyAction
import com.tangem.tap.features.send.redux.AmountAction
import com.tangem.tap.features.send.redux.AmountActionUi
import com.tangem.tap.features.send.redux.FeeAction
import com.tangem.tap.features.send.redux.FeeActionUi
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.send.redux.ReceiptAction
import com.tangem.tap.features.send.redux.ReleaseSendState
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.TransactionExtrasAction
import com.tangem.tap.features.send.redux.states.ExternalTransactionData
import com.tangem.tap.features.send.redux.states.IdStateHolder
import com.tangem.tap.features.send.redux.states.SendState
@ -46,7 +57,8 @@ private class SendReducer : SendInternalReducer {
sendState.copy(sendButtonState = IndeterminateProgressButton(action.state))
}
is SendAction.Dialog.TezosWarningDialog -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails.CardSdkError -> sendState.copy(dialog = action)
is SendAction.Dialog.SendTransactionFails.BlockchainSdkError -> sendState.copy(dialog = action)
is SendAction.Dialog.Hide -> sendState.copy(dialog = null)
is SendAction.Warnings.Set -> sendState.copy(sendWarningsList = action.warningList)
is SendAction.SendSpecificTransaction ->

View file

@ -7,9 +7,6 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.blockchain.common.WalletManager
import com.tangem.blockchain.common.address.AddressType
import com.tangem.common.extensions.isZero
import com.tangem.domain.common.extensions.canHandleToken
import com.tangem.domain.common.extensions.toCoinId
import com.tangem.domain.features.addCustomToken.CustomCurrency
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.extensions.toQrCode
import com.tangem.tap.common.redux.global.CryptoCurrencyName
@ -17,12 +14,18 @@ import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.*
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.TotalBalance
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.models.WalletWarning
import com.tangem.tap.features.wallet.models.hasPendingTransactions
import com.tangem.tap.features.wallet.models.hasSendableAmounts
import com.tangem.tap.features.wallet.models.isSendableAmount
import com.tangem.tap.features.wallet.redux.reducers.calculateTotalFiatAmount
import com.tangem.tap.features.wallet.redux.reducers.findProgressState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.network.exchangeServices.CurrencyExchangeManager
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
@ -48,21 +51,15 @@ data class WalletState(
// if you do not delegate - the application crashes on startup,
// because twinCardsState has not been created yet
val twinCardsState: TwinCardsState by ReadOnlyProperty<Any, TwinCardsState> { thisRef, property ->
val twinCardsState: TwinCardsState by ReadOnlyProperty<Any, TwinCardsState> { _, _ ->
store.state.twinCardsState
}
val isTangemTwins: Boolean
get() = store.state.globalState.scanResponse?.isTangemTwins() == true
val primaryWallet: WalletData? = wallets.firstOrNull()
?.walletsData?.firstOrNull()
val primaryWalletManager: WalletManager? =
if (wallets.isNotEmpty()) wallets[0].walletManager else null
val shouldShowDetails: Boolean =
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
val isExchangeServiceFeatureOn: Boolean
get() = store.state.globalState.exchangeManager.featureIsSwitchedOn()
val blockchains: List<Blockchain>
get() = wallets.mapNotNull { it.walletManager?.wallet?.blockchain }
@ -76,6 +73,14 @@ data class WalletState(
val walletManagers: List<WalletManager>
get() = wallets.mapNotNull { it.walletManager }
val primaryWallet: WalletData? = wallets.firstOrNull()?.walletsData?.firstOrNull()
val primaryWalletManager: WalletManager? = if (wallets.isNotEmpty()) wallets[0].walletManager else null
val shouldShowDetails: Boolean =
primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
primaryWallet?.currencyData?.status != BalanceStatus.UnknownBlockchain
fun getWalletManager(currency: Currency?): WalletManager? {
if (currency?.blockchain == null) return null
return getWalletStore(currency)?.walletManager
@ -248,32 +253,6 @@ data class WalletState(
return updatedWallets + remainingWallets
}
fun updateTradeCryptoState(
exchangeManager: CurrencyExchangeManager?,
walletData: WalletData
): WalletData {
return walletData.copy(
tradeCryptoState = TradeCryptoState.from(
exchangeManager,
walletData
)
)
}
fun updateTradeCryptoState(
exchangeManager: CurrencyExchangeManager?,
walletDataList: List<WalletData>
): List<WalletData> {
return walletDataList.map {
it.copy(
tradeCryptoState = TradeCryptoState.from(
exchangeManager,
it
)
)
}
}
private fun updateTotalBalance(): WalletState {
val walletsData = this.wallets
.flatMap(WalletStore::walletsData)
@ -352,39 +331,24 @@ data class Artwork(
}
}
data class TradeCryptoState(
val isAvailableToSell: () -> Boolean = { false },
val isAvailableToBuy: () -> Boolean = { false },
) {
companion object {
fun from(
exchangeManager: CurrencyExchangeManager?,
walletData: WalletData
): TradeCryptoState {
val exchanger = exchangeManager ?: return walletData.tradeCryptoState
val currency = walletData.currency
return TradeCryptoState(
isAvailableToSell = { exchanger.availableForSell(currency) },
isAvailableToBuy = { exchanger.availableForBuy(currency) },
)
}
}
}
data class WalletData(
val pendingTransactions: List<PendingTransaction> = emptyList(),
val hashesCountVerified: Boolean? = null,
val walletAddresses: WalletAddresses? = null,
val currencyData: BalanceWidgetData = BalanceWidgetData(),
val updatingWallet: Boolean = false,
val tradeCryptoState: TradeCryptoState = TradeCryptoState(),
val fiatRateString: String? = null,
val fiatRate: BigDecimal? = null,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val currency: Currency,
val walletRent: WalletRent? = null,
) {
val isAvailableToBuy: Boolean
get() = store.state.globalState.exchangeManager.availableForBuy(currency)
val isAvailableToSell: Boolean
get() = store.state.globalState.exchangeManager.availableForSell(currency)
fun shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list ?: return false
return listOfAddresses.size > 1

View file

@ -37,20 +37,18 @@ class TradeCryptoMiddleware {
action: WalletAction.TradeCryptoAction.Buy,
) {
if (action.checkUserLocation && state()?.globalState?.userCountryCode == RUSSIA_COUNTRY_CODE) {
store.dispatchOnMain(
WalletAction.DialogAction.RussianCardholdersWarningDialog
)
store.dispatchOnMain(WalletAction.DialogAction.RussianCardholdersWarningDialog)
return
}
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val exchangeManager = store.state.globalState.exchangeManager ?: return
val card = store.state.globalState.scanResponse?.card ?: return
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
if (addresses.isEmpty()) return
val exchangeManager = store.state.globalState.exchangeManager
val appCurrency = store.state.globalState.appCurrency
val currency = selectedWalletData.currency
if (currency is Currency.Token && currency.blockchain.isTestnet()) {
@ -81,15 +79,13 @@ class TradeCryptoMiddleware {
private fun proceedSellAction() {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val exchangeManager = store.state.globalState.exchangeManager ?: return
val appCurrency = store.state.globalState.appCurrency
val appCurrency = store.state.globalState.appCurrency
val addresses = selectedWalletData.walletAddresses?.list.orEmpty()
if (addresses.isEmpty()) return
val currency = selectedWalletData.currency
exchangeManager.getUrl(
store.state.globalState.exchangeManager.getUrl(
action = CurrencyExchangeManager.Action.Sell,
blockchain = currency.blockchain,
cryptoCurrencyName = currency.currencySymbol,
@ -100,8 +96,8 @@ class TradeCryptoMiddleware {
private fun preconfigureAndOpenSendScreen(action: WalletAction.TradeCryptoAction.SendCrypto) {
val selectedWalletData = store.state.walletState.getSelectedWalletData() ?: return
val walletManager =
store.state.walletState.getWalletManager(selectedWalletData.currency)
val walletManager = store.state.walletState.getWalletManager(selectedWalletData.currency)
store.dispatchOnMain(PrepareSendScreen(
coinAmount = walletManager?.wallet?.amounts?.get(AmountType.Coin),
coinRate = selectedWalletData.fiatRate,
@ -116,11 +112,10 @@ class TradeCryptoMiddleware {
}
private fun openReceiptUrl(transactionId: String) {
val exchangeManager = store.state.globalState.exchangeManager ?: return
store.dispatchOnMain(NavigationAction.PopBackTo())
exchangeManager.getSellCryptoReceiptUrl(CurrencyExchangeManager.Action.Sell, transactionId)?.let {
store.dispatchOnMain(NavigationAction.OpenUrl(it))
}
store.state.globalState.exchangeManager.getSellCryptoReceiptUrl(
action = CurrencyExchangeManager.Action.Sell,
transactionId = transactionId,
)?.let { store.dispatchOnMain(NavigationAction.OpenUrl(it)) }
}
}

View file

@ -13,7 +13,9 @@ import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.filterByToken
import com.tangem.tap.features.wallet.models.getPendingTransactions
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletState.Companion.UNKNOWN_AMOUNT_SIGN
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
@ -38,8 +40,6 @@ class OnWalletLoadedReducer {
val walletData = walletState.getWalletData(blockchainNetwork) ?: return walletState
val fiatCurrency = store.state.globalState.appCurrency
val exchangeManager = store.state.globalState.exchangeManager
val coinAmountValue = wallet.amounts[AmountType.Coin]?.value
val formattedAmount = coinAmountValue?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
@ -71,7 +71,6 @@ class OnWalletLoadedReducer {
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(isCoinSendButtonEnabled),
currency = Currency.fromBlockchainNetwork(blockchainNetwork),
tradeCryptoState = TradeCryptoState.from(exchangeManager, walletData),
)
val tokens = wallet.getTokens().mapNotNull { token ->
@ -104,7 +103,6 @@ class OnWalletLoadedReducer {
),
pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(isTokenSendButtonEnabled),
tradeCryptoState = TradeCryptoState.from(exchangeManager, tokenWalletData),
)
}
val newWallets = tokens + newWalletData
@ -118,8 +116,6 @@ class OnWalletLoadedReducer {
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
val fiatCurrencyName = store.state.globalState.appCurrency.code
val exchangeManager = store.state.globalState.exchangeManager
val token = wallet.getFirstToken()
val tokenData = if (token != null) {
val tokenAmount = wallet.getTokenAmount(token)
@ -167,7 +163,6 @@ class OnWalletLoadedReducer {
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
tradeCryptoState = TradeCryptoState.from(exchangeManager, walletState.primaryWallet),
)
val wallets = listOfNotNull(walletData)
val updatedStore = walletState.getWalletStore(walletData?.currency)?.updateWallets(wallets)

View file

@ -6,7 +6,11 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.mapNotNullValues
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.entities.FiatCurrency
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.extensions.toFiatRateString
import com.tangem.tap.common.extensions.toFiatString
import com.tangem.tap.common.extensions.toFiatValue
import com.tangem.tap.common.extensions.toFormattedCurrencyString
import com.tangem.tap.common.extensions.toFormattedFiatValue
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.getArtworkUrl
@ -14,10 +18,18 @@ import com.tangem.tap.domain.getFirstToken
import com.tangem.tap.domain.tokens.models.BlockchainNetwork
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.WalletRent
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Artwork
import com.tangem.tap.features.wallet.redux.ErrorType
import com.tangem.tap.features.wallet.redux.ProgressState
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletAddresses
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.redux.WalletStore
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.store
import org.rekotlin.Action
import java.math.BigDecimal
@ -35,7 +47,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
if (action !is WalletAction) return state.walletState
val exchangeManager = store.state.globalState.exchangeManager
var newState = state.walletState
when (action) {
@ -144,10 +155,6 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
currencySymbol = walletData.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
tradeCryptoState = TradeCryptoState.from(
exchangeManager,
walletData
)
)
}
)
@ -171,13 +178,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
currencySymbol = wallet.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
tradeCryptoState = TradeCryptoState.from(exchangeManager, wallet)
)
}
val wallets = newState.updateTradeCryptoState(
exchangeManager,
newState.replaceSomeWallets(newWallets)
)
val wallets = newState.replaceSomeWallets(newWallets)
val walletStore = newState.getWalletStore(action.blockchain)?.updateWallets(wallets)
newState = newState.updateWalletStore(walletStore)
}
@ -210,14 +213,9 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
)
}
var updatedWalletStore = newState.getWalletStore(action.blockchain)
val updatedWalletStore = newState.getWalletStore(action.blockchain)
?.updateWallets(listOfNotNull(walletData))
updatedWalletStore =
updatedWalletStore?.updateWallets(
newState.updateTradeCryptoState(exchangeManager, updatedWalletStore.walletsData)
)
newState = newState.updateWalletStore(updatedWalletStore)
}
@ -248,11 +246,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
)
}
val updatedWallets =
newState.updateTradeCryptoState(
exchangeManager,
walletStore!!.updateWallets(listOfNotNull(newWalletData) + tokenWallets).walletsData
)
val updatedWallets = walletStore!!.updateWallets(listOfNotNull(newWalletData) + tokenWallets).walletsData
newState = newState.updateWalletsData(updatedWallets)

View file

@ -140,7 +140,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
setupAddressCard(selectedWallet)
setupNoInternetHandling(state)
setupBalanceData(selectedWallet.currencyData)
setupButtons(selectedWallet)
setupButtons(selectedWallet, state.isExchangeServiceFeatureOn)
handleCurrencyIcon(selectedWallet)
handleWarnings(selectedWallet)
@ -186,7 +186,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
)
}
private fun setupButtons(selectedWallet: WalletData) = with(binding) {
private fun setupButtons(selectedWallet: WalletData, isExchangeServiceFeatureOn: Boolean) = with(binding) {
lWalletDetails.btnCopy.setOnClickListener {
selectedWallet.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.CopyAddress(addressString, requireContext()))
@ -199,8 +199,9 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details),
}
rowButtons.updateButtonsVisibility(
buyAllowed = selectedWallet.tradeCryptoState.isAvailableToBuy(),
sellAllowed = selectedWallet.tradeCryptoState.isAvailableToSell(),
exchangeServiceFeatureOn = isExchangeServiceFeatureOn,
buyAllowed = selectedWallet.isAvailableToBuy,
sellAllowed = selectedWallet.isAvailableToSell,
sendAllowed = selectedWallet.mainButton.enabled,
)
}

View file

@ -114,22 +114,26 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun newState(state: WalletState) {
if (activity == null || view == null) return
val isSaltPay = store.state.globalState.scanResponse?.card?.isSaltPay == true
when {
isSaltPay -> {
isSaltPay && (walletView !is SaltPaySingleWalletView) -> {
walletView = SaltPaySingleWalletView()
walletView.changeWalletView(this, binding)
}
state.isMultiwalletAllowed &&
state.primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
walletView is SingleWalletView -> {
state.isMultiwalletAllowed && state.primaryWallet?.currencyData?.status != BalanceStatus.EmptyCard &&
walletView !is MultiWalletView -> {
walletView = MultiWalletView()
walletView.changeWalletView(this, binding)
}
!state.isMultiwalletAllowed && walletView is MultiWalletView -> {
!state.isMultiwalletAllowed && walletView !is SingleWalletView -> {
walletView = SingleWalletView()
walletView.changeWalletView(this, binding)
}
else -> {} // we keep the same view unless we scan a card that requires a different view
}
walletView.changeWalletView(this, binding)
walletView.onNewState(state)
if (!state.shouldShowDetails) {

View file

@ -14,9 +14,8 @@ internal class WalletDetailsButtonsRow @JvmOverloads constructor(
) : LinearLayout(context, attrs, defStyleAttr) {
private val binding = ViewWalletDetailsButtonsRowBinding.inflate(
LayoutInflater.from(context),
this
this,
)
var onBuyClick: (() -> Unit)? = null
var onSellClick: (() -> Unit)? = null
var onTradeClick: (() -> Unit)? = null
@ -34,10 +33,13 @@ internal class WalletDetailsButtonsRow @JvmOverloads constructor(
}
fun updateButtonsVisibility(
exchangeServiceFeatureOn: Boolean,
buyAllowed: Boolean,
sellAllowed: Boolean,
sendAllowed: Boolean
sendAllowed: Boolean,
) = with(binding) {
containerExchangeButtons.isVisible = exchangeServiceFeatureOn
btnBuy.isVisible = (buyAllowed && !sellAllowed) || (!buyAllowed && !sellAllowed)
btnBuy.isEnabled = buyAllowed
btnSell.isVisible = !buyAllowed && sellAllowed

View file

@ -1,5 +1,6 @@
package com.tangem.tap.features.wallet.ui.wallet
import android.widget.Button
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.common.card.Card
@ -20,11 +21,11 @@ import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.WalletAdapter
import com.tangem.tap.features.wallet.ui.view.WalletDetailsButtonsRow
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
class MultiWalletView : WalletView() {
private lateinit var walletsAdapter: WalletAdapter
override fun changeWalletView(fragment: WalletFragment, binding: FragmentWalletBinding) {
@ -33,14 +34,12 @@ class MultiWalletView : WalletView() {
showMultiWalletView(binding)
}
private fun showMultiWalletView(binding: FragmentWalletBinding) = with(binding) {
tvTwinCardNumber.hide()
rvPendingTransaction.hide()
lCardBalance.root.hide()
lAddress.root.hide()
lButtonsShort.root.hide()
lButtonsLong.root.hide()
rowButtons.hide()
lSingleWalletBalance.root.hide()
rvMultiwallet.show()
btnAddToken.show()
@ -59,7 +58,6 @@ class MultiWalletView : WalletView() {
}
}
override fun onViewCreated() {
setupWalletsRecyclerView()
}
@ -86,17 +84,17 @@ class MultiWalletView : WalletView() {
TokensAction.LoadCurrencies(
supportedBlockchains = currenciesRepository.getBlockchains(
card.firmwareVersion,
card.isTestCard
card.isTestCard,
),
scanResponse = store.state.globalState.scanResponse
)
scanResponse = store.state.globalState.scanResponse,
),
)
store.dispatch(TokensAction.AllowToAddTokens(true))
store.dispatch(
TokensAction.SetAddedCurrencies(
wallets = state.walletsData,
derivationStyle = card.derivationStyle
)
derivationStyle = card.derivationStyle,
),
)
store.dispatch(NavigationAction.NavigateTo(AppScreen.AddTokens))
}
@ -105,7 +103,7 @@ class MultiWalletView : WalletView() {
private fun handleBackupWarning(
binding: FragmentWalletBinding,
showBackupWarning: Boolean
showBackupWarning: Boolean,
) = with(binding.lWalletBackupWarning) {
root.isVisible = showBackupWarning
root.setOnClickListener {
@ -128,11 +126,11 @@ class MultiWalletView : WalletView() {
veilBalance.unVeil()
}
tvProcessing.animateVisibility(
show = totalBalance.state == ProgressState.Error
show = totalBalance.state == ProgressState.Error,
)
tvBalance.text = totalBalance.fiatAmount.formatAmountAsSpannedString(
currencySymbol = totalBalance.fiatCurrency.symbol
currencySymbol = totalBalance.fiatCurrency.symbol,
)
tvCurrencyName.text = totalBalance.fiatCurrency.code
@ -145,14 +143,14 @@ class MultiWalletView : WalletView() {
private fun handleErrorStates(
state: WalletState,
binding: FragmentWalletBinding,
fragment: WalletFragment
fragment: WalletFragment,
) {
when (state.primaryWallet?.currencyData?.status) {
BalanceStatus.EmptyCard -> {
showErrorState(
binding,
fragment.getText(R.string.wallet_error_empty_card),
fragment.getString(R.string.wallet_error_empty_card_subtitle)
fragment.getString(R.string.wallet_error_empty_card_subtitle),
)
configureButtonsForEmptyWalletState(binding)
}
@ -160,7 +158,7 @@ class MultiWalletView : WalletView() {
showErrorState(
binding,
fragment.getText(R.string.wallet_error_unsupported_blockchain),
fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle)
fragment.getString(R.string.wallet_error_unsupported_blockchain_subtitle),
)
}
else -> { /* no-op */
@ -184,9 +182,21 @@ class MultiWalletView : WalletView() {
private fun configureButtonsForEmptyWalletState(binding: FragmentWalletBinding) =
with(binding) {
lButtonsLong.root.show()
lButtonsLong.btnConfirmLong.setOnClickListener { store.dispatch(WalletAction.CreateWallet) }
lButtonsLong.btnConfirmLong.text =
fragment?.getText(R.string.wallet_button_create_wallet)
rowButtons.btnBuy.hide()
rowButtons.btnSell.hide()
rowButtons.btnTrade.hide()
rowButtons.show()
rowButtons.btnSend.text = fragment?.getText(R.string.wallet_button_create_wallet)
rowButtons.onSendClick = { store.dispatch(WalletAction.CreateWallet) }
}
}
}
private val WalletDetailsButtonsRow.btnBuy: Button
get() = this.findViewById(R.id.btn_buy)
private val WalletDetailsButtonsRow.btnSell: Button
get() = this.findViewById(R.id.btn_sell)
private val WalletDetailsButtonsRow.btnTrade: Button
get() = this.findViewById(R.id.btn_trade)
private val WalletDetailsButtonsRow.btnSend: Button
get() = this.findViewById(R.id.btn_send)

View file

@ -2,7 +2,6 @@ package com.tangem.tap.features.wallet.ui.wallet
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import androidx.recyclerview.widget.LinearLayoutManager
import com.tangem.domain.common.TwinCardNumber
import com.tangem.tap.common.extensions.beginDelayedTransition
@ -12,11 +11,15 @@ import com.tangem.tap.common.extensions.show
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.Currency
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.features.wallet.redux.WalletData
import com.tangem.tap.features.wallet.redux.WalletMainButton
import com.tangem.tap.features.wallet.redux.WalletState
import com.tangem.tap.features.wallet.ui.BalanceWidget
import com.tangem.tap.features.wallet.ui.MultipleAddressUiHelper
import com.tangem.tap.features.wallet.ui.WalletFragment
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
import com.tangem.tap.features.wallet.ui.view.WalletDetailsButtonsRow
import com.tangem.tap.store
import com.tangem.wallet.R
import com.tangem.wallet.databinding.FragmentWalletBinding
@ -55,7 +58,7 @@ class SingleWalletView : WalletView() {
state.primaryWallet ?: return
setupTwinCards(state.twinCardsState, binding)
setupButtons(state.primaryWallet, state.isTangemTwins, binding)
setupButtons(state.primaryWallet, binding, state.isExchangeServiceFeatureOn)
setupAddressCard(state.primaryWallet, binding)
showPendingTransactionsIfPresent(state.primaryWallet.pendingTransactions)
setupBalance(state, state.primaryWallet)
@ -79,9 +82,7 @@ class SingleWalletView : WalletView() {
}
}
private fun setupTwinCards(
twinCardsState: TwinCardsState?, binding: FragmentWalletBinding,
) = with(binding) {
private fun setupTwinCards(twinCardsState: TwinCardsState?, binding: FragmentWalletBinding) = with(binding) {
twinCardsState?.cardNumber?.let { cardNumber ->
tvTwinCardNumber.show()
val number = when (cardNumber) {
@ -97,92 +98,47 @@ class SingleWalletView : WalletView() {
}
private fun setupButtons(
state: WalletData, isTwinsWallet: Boolean, binding: FragmentWalletBinding,
walletData: WalletData,
binding: FragmentWalletBinding,
isExchangeServiceFeatureEnabled: Boolean,
) = with(binding) {
setupButtonsType(state, binding)
val tradeState = state.tradeCryptoState
val btnConfirm = if (tradeState.isAvailableToSell() || tradeState.isAvailableToBuy()) {
lButtonsShort.btnConfirm
} else {
lButtonsLong.btnConfirmLong
}
setupConfirmButton(state, btnConfirm, isTwinsWallet)
setupRowButtons(walletData, rowButtons, isExchangeServiceFeatureEnabled)
lAddress.btnCopy.setOnClickListener {
state.walletAddresses?.selectedAddress?.address?.let { addressString ->
walletData.walletAddresses?.selectedAddress?.address?.let { addressString ->
store.dispatch(WalletAction.CopyAddress(addressString, fragment!!.requireContext()))
}
}
lAddress.btnShowQr.setOnClickListener {
state.walletAddresses?.selectedAddress?.let { selectedAddress ->
walletData.walletAddresses?.selectedAddress?.let { selectedAddress ->
store.dispatch(
WalletAction.DialogAction.QrCode(
currency = state.currency,
currency = walletData.currency,
selectedAddress = selectedAddress,
),
)
}
}
setupTradeButton(binding, state.tradeCryptoState)
}
private fun setupTradeButton(binding: FragmentWalletBinding, tradeCryptoState: TradeCryptoState) {
val allowedToBuy = tradeCryptoState.isAvailableToBuy()
val allowedToSell = tradeCryptoState.isAvailableToSell()
val action = when {
allowedToBuy && !allowedToSell -> WalletAction.TradeCryptoAction.Buy()
!allowedToBuy && allowedToSell -> WalletAction.TradeCryptoAction.Sell
allowedToBuy && allowedToSell -> WalletAction.DialogAction.ChooseTradeActionDialog
else -> null
}
val text = when {
allowedToBuy && !allowedToSell -> R.string.wallet_button_buy
!allowedToBuy && allowedToSell -> R.string.wallet_button_sell
allowedToBuy && allowedToSell -> R.string.wallet_button_trade
else -> R.string.wallet_button_trade
}
val icon = when {
allowedToBuy && !allowedToSell -> R.drawable.ic_arrow_up
!allowedToBuy && allowedToSell -> R.drawable.ic_arrow_down
allowedToBuy && allowedToSell -> R.drawable.ic_arrows_up_down
else -> null
}
with(binding) {
lButtonsShort.btnTrade.text = fragment?.getText(text)
icon?.let { lButtonsShort.btnTrade.setIconResource(it) }
lButtonsShort.btnTrade.setOnClickListener { if (action != null) store.dispatch(action) }
}
}
private fun setupButtonsType(state: WalletData, binding: FragmentWalletBinding) = with(binding) {
if (state.tradeCryptoState.isAvailableToSell() || state.tradeCryptoState.isAvailableToBuy()) {
lButtonsLong.root.hide()
lButtonsShort.root.show()
} else {
lButtonsLong.root.show()
lButtonsShort.root.hide()
}
}
private fun setupConfirmButton(
state: WalletData, btnConfirm: Button, isTwinsWallet: Boolean,
private fun setupRowButtons(
walletData: WalletData,
rowButtons: WalletDetailsButtonsRow,
isExchangeServiceFeatureEnabled: Boolean,
) {
val buttonTitle = when (state.mainButton) {
is WalletMainButton.SendButton -> R.string.wallet_button_send
is WalletMainButton.CreateWalletButton -> {
if (!isTwinsWallet) {
R.string.wallet_button_create_wallet
} else {
R.string.wallet_button_create_twin_wallet
}
}
}
btnConfirm.text = fragment?.getString(buttonTitle)
btnConfirm.isEnabled = state.mainButton.enabled
btnConfirm.setOnClickListener {
when (state.mainButton) {
rowButtons.updateButtonsVisibility(
exchangeServiceFeatureOn = isExchangeServiceFeatureEnabled,
buyAllowed = walletData.isAvailableToBuy,
sellAllowed = walletData.isAvailableToSell,
sendAllowed = walletData.mainButton.enabled,
)
rowButtons.onBuyClick = { store.dispatch(WalletAction.TradeCryptoAction.Buy()) }
rowButtons.onSendClick = { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
rowButtons.onTradeClick = { store.dispatch(WalletAction.DialogAction.ChooseTradeActionDialog) }
rowButtons.onSendClick = {
when (walletData.mainButton) {
is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send())
is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet)
}

View file

@ -0,0 +1,60 @@
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 featureIsSwitchedOn(): Boolean {
val card = cardProvider() ?: return false
return !card.isStart2Coin
}
override fun isBuyAllowed(): Boolean {
val card = cardProvider() ?: return false
return when {
card.isDemoCard() -> true
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() -> true
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,17 +23,26 @@ import java.math.BigDecimal
class CurrencyExchangeManager(
private val buyService: ExchangeService,
private val sellService: ExchangeService,
private val primaryRules: ExchangeRules,
) : ExchangeService, ExchangeUrlBuilder {
override fun featureIsSwitchedOn(): Boolean = primaryRules.featureIsSwitchedOn()
override suspend fun update() {
buyService.update()
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,
@ -67,6 +76,14 @@ class CurrencyExchangeManager(
}
enum class Action { Buy, Sell }
companion object {
fun dummy(): CurrencyExchangeManager = CurrencyExchangeManager(
buyService = ExchangeService.dummy(),
sellService = ExchangeService.dummy(),
primaryRules = ExchangeRules.dummy(),
)
}
}
suspend fun CurrencyExchangeManager.buyErc20TestnetTokens(

View file

@ -1,16 +1,44 @@
package com.tangem.tap.network.exchangeServices
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.common.feature.Feature
import com.tangem.tap.features.wallet.models.Currency
interface ExchangeService {
suspend fun update()
interface Exchanger {
fun isBuyAllowed(): Boolean
fun isSellAllowed(): Boolean
fun availableForBuy(currency: Currency):Boolean
fun availableForSell(currency: Currency):Boolean
}
interface ExchangeService: Feature, Exchanger {
suspend fun update()
companion object {
fun dummy(): ExchangeService = object : ExchangeService {
override fun featureIsSwitchedOn(): Boolean = false
override suspend fun update() {}
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean = false
}
}
}
interface ExchangeRules: Feature, Exchanger {
companion object {
fun dummy(): ExchangeRules = object : ExchangeRules {
override fun featureIsSwitchedOn(): Boolean = false
override fun isBuyAllowed(): Boolean = false
override fun isSellAllowed(): Boolean = false
override fun availableForBuy(currency: Currency): Boolean = false
override fun availableForSell(currency: Currency): Boolean = false
}
}
}
interface ExchangeUrlBuilder {
fun getUrl(
action: CurrencyExchangeManager.Action,

View file

@ -4,15 +4,6 @@ import com.squareup.moshi.Json
import retrofit2.http.GET
import retrofit2.http.Path
/**
[REDACTED_AUTHOR]
*/
private val CurrenciesUrl = "https://api.mercuryo.io/v1.6/lib/currencies"
interface MercuryoApi {

View file

@ -28,6 +28,8 @@ class MercuryoService(
private val blockchainsAvailableToBuy = mutableListOf<Blockchain>()
private val tokensAvailableToBy = mutableMapOf<String, MutableList<Blockchain>>()
override fun featureIsSwitchedOn(): Boolean = true
override suspend fun update() {
when (val result = performRequest { api.currencies(apiVersion) }) {
is Result.Success -> {
@ -130,6 +132,7 @@ class MercuryoService(
private fun blockchainFromCurrencyName(currencyName: String): Blockchain? = when (currencyName) {
"BNB" -> Blockchain.BSC
"ETH" -> Blockchain.Ethereum
"ADA" -> Blockchain.CardanoShelley
else -> Blockchain.values().find { it.currency.lowercase() == currencyName.lowercase() }
}
}

View file

@ -29,6 +29,8 @@ class MoonPayService(
private var status: MoonPayStatus? = null
override fun featureIsSwitchedOn(): Boolean = true
override suspend fun update() {
withIOContext {
performRequest {

View file

@ -5,7 +5,7 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.tangem.common.json.MoshiJsonConverter
import java.util.Calendar
import java.util.*
class PreferencesStorage(applicationContext: Application) {
@ -25,6 +25,10 @@ class PreferencesStorage(applicationContext: Application) {
fiatCurrenciesPrefStorage.migrate()
}
var chatFirstLaunchTime: Long?
get() = preferences.getLong(CHAT_FIRST_LAUNCH_KEY, 0).takeIf { it != 0L }
set(value) = preferences.edit { putLong(CHAT_FIRST_LAUNCH_KEY, value ?: 0) }
fun getCountOfLaunches(): Int = preferences.getInt(APP_LAUNCH_COUNT_KEY, 1)
@Deprecated("Use UsedCardsPrefStorage instead")
@ -58,6 +62,7 @@ class PreferencesStorage(applicationContext: Application) {
private const val DISCLAIMER_ACCEPTED_KEY = "disclaimerAccepted"
private const val TWINS_ONBOARDING_SHOWN_KEY = "twinsOnboardingShown"
private const val APP_LAUNCH_COUNT_KEY = "launchCount"
private const val CHAT_FIRST_LAUNCH_KEY = "chatFirstLaunchKey"
}
}

View file

@ -6,6 +6,7 @@
android:layout_height="match_parent"
android:background="@color/backgroundWhite"
android:clipChildren="false"
android:clipToPadding="false"
android:fitsSystemWindows="true"
android:orientation="vertical">

View file

@ -47,7 +47,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:paddingBottom="74dp">
android:paddingBottom="92dp">
<ImageView
android:id="@+id/iv_card"
@ -93,6 +93,17 @@
app:barrierDirection="bottom"
app:constraint_referenced_ids="iv_card,tv_twin_card_number" />
<include
android:id="@+id/l_wallet_backup_warning"
layout="@layout/layout_wallet_backup_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_warning_messages"
android:layout_width="match_parent"
@ -105,17 +116,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier" />
<include
android:id="@+id/l_wallet_backup_warning"
layout="@layout/layout_wallet_backup_warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/rv_warning_messages" />
<include
android:id="@+id/l_card_total_balance"
layout="@layout/layout_card_total_balance"
@ -176,32 +176,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_pending_transaction" />
<include
android:id="@+id/l_buttons_long"
layout="@layout/layout_wallet_long_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1" />
<include
android:id="@+id/l_buttons_short"
layout="@layout/layout_wallet_short_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/l_address"
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_add_token"
style="@style/BaseTapButton"
@ -213,6 +187,7 @@
android:elevation="4dp"
android:text="@string/main_manage_tokens"
android:textSize="16sp"
android:visibility="gone"
app:cornerRadius="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/rv_multiwallet"
@ -222,4 +197,14 @@
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<com.tangem.tap.features.wallet.ui.view.WalletDetailsButtonsRow
android:id="@+id/row_buttons"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="32dp" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,50 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:parentTag="android.widget.LinearLayout"
tools:orientation="horizontal">
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:orientation="horizontal"
tools:parentTag="android.widget.LinearLayout">
<com.google.android.material.button.MaterialButton
<FrameLayout
android:id="@+id/container_exchange_buttons"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_weight="1">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_trade"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/wallet_button_trade"
android:visibility="gone"
app:icon="@drawable/ic_arrows_up_down"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_buy"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/wallet_button_buy"
android:visibility="gone"
app:icon="@drawable/ic_arrow_up" />
<com.google.android.material.button.MaterialButton
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sell"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginEnd="6dp"
android:layout_weight="1"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/wallet_button_sell"
android:visibility="gone"
app:icon="@drawable/ic_arrow_down" />
</FrameLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_send"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:text="@string/wallet_button_send"
app:icon="@drawable/ic_send" />
android:id="@+id/btn_send"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_weight="1"
android:text="@string/wallet_button_send"
app:icon="@drawable/ic_send" />
</merge>

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>
@ -100,7 +100,7 @@
<string name="wallet_connect_subtitle">Подключение к Dapps</string>
<string name="details_row_title_create_backup">Добавить еще карты</string>
<string name="details_row_title_create_backup_footer">Вы можете объеденить до трех карт в одним кошельке. Это можно сделать только один раз.</string>
<string name="details_row_title_create_backup_footer">Вы можете объединить до трех карт в одном кошельке. Это можно сделать только один раз.</string>
<string name="details_row_privacy_policy">Privacy policy</string>

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="ZendeskTheme" parent="ZendeskSdkTheme.Light">
<item name="android:forceDarkAllowed">false</item>
</style>
</resources>

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>

View file

@ -2,7 +2,7 @@ ext.versions = [
kotlin : '1.6.10',
build_gradle : '7.1.3',
tangem_card_sdk : 'develop-159',
tangem_blockchain_sdk: 'develop-100',
tangem_blockchain_sdk: 'develop-105',
// tangem_blockchain_sdk: '0.0.1',
]
@ -10,4 +10,4 @@ ext.environmentConfig = [
environment : "ENVIRONMENT",
testActionEnabled: "TEST_ACTION_ENABLED",
logEnabled : "LOG_ENABLED",
]
]