Updated on 2026-08-14

This commit is contained in:
Tangem 2020-09-17 11:48:03 +00:00
commit 01e4ad8abb
40 changed files with 1100 additions and 71 deletions

8
.idea/dictionaries/romanpotapov.xml generated Normal file
View file

@ -0,0 +1,8 @@
<component name="ProjectDictionaryState">
<dictionary name="romanpotapov">
<words>
<w>blockchain</w>
<w>tangem</w>
</words>
</dictionary>
</component>

View file

@ -4,6 +4,7 @@ import android.app.Application
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.appReducer
import com.tangem.tap.network.NetworkConnectivity
import com.tangem.tap.persistence.PreferencesStorage
import com.tangem.wallet.BuildConfig
import org.rekotlin.Store
import timber.log.Timber
@ -13,10 +14,12 @@ val store = Store(
middleware = AppState.getMiddleware(),
state = AppState()
)
lateinit var preferencesStorage: PreferencesStorage
class TapApplication : Application() {
override fun onCreate() {
super.onCreate()
preferencesStorage = PreferencesStorage(this)
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())

View file

@ -5,6 +5,6 @@ package com.tangem.tap.common.entities
*/
class TapCurrency {
companion object{
val main = "USD"
const val DEFAULT_FIAT_CURRENCY = "USD"
}
}

View file

@ -4,6 +4,8 @@ import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.features.details.ui.DetailsConfirmFragment
import com.tangem.tap.features.details.ui.DetailsFragment
import com.tangem.tap.features.home.HomeFragment
import com.tangem.tap.features.send.ui.SendFragment
import com.tangem.tap.features.wallet.ui.WalletFragment
@ -36,5 +38,7 @@ private fun fragmentFactory(screen: AppScreen): Fragment {
AppScreen.Home -> HomeFragment()
AppScreen.Wallet -> WalletFragment()
AppScreen.Send -> SendFragment()
AppScreen.Details -> DetailsFragment()
AppScreen.DetailsConfirm -> DetailsConfirmFragment()
}
}

View file

@ -6,6 +6,8 @@ import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import java.math.BigDecimal
import java.math.RoundingMode
import java.text.DecimalFormat
@ -46,10 +48,10 @@ fun BigDecimal.toFormattedString(decimals: Int): String {
return df.format(bd)
}
fun BigDecimal.toFiatString(rateValue: BigDecimal): String? {
fun BigDecimal.toFiatString(rateValue: BigDecimal, fiatCurrencyName: FiatCurrencyName): String? {
var fiatValue = rateValue.multiply(this)
fiatValue = fiatValue.setScale(2, RoundingMode.DOWN)
return "≈ USD $fiatValue"
return "≈ ${fiatCurrencyName} $fiatValue"
}
fun BigDecimal.stripZeroPlainString(): String = this.stripTrailingZeros().toPlainString()
@ -67,4 +69,6 @@ fun BigDecimal.isGreaterThanOrEqual(value: BigDecimal): Boolean {
fun BigDecimal.isLessThanOrEqual(value: BigDecimal): Boolean {
val compareResult = this.compareTo(value)
return compareResult == -1 || compareResult == 0
}
}
fun FiatCurrency.toFormattedString(): String = "${this.name} (${this.symbol}) - ${this.sign}"

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.globalReducer
import com.tangem.tap.common.redux.navigation.NavigationReducer
import com.tangem.tap.features.send.redux.reducers.SendScreenReducer
import com.tangem.tap.features.details.redux.DetailsReducer
import com.tangem.tap.features.wallet.redux.WalletReducer
import org.rekotlin.Action
@ -11,10 +12,11 @@ fun appReducer(action: Action, state: AppState?): AppState {
if (action is AppAction.RestoreState) return action.state
return AppState(
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
walletState = WalletReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState)
navigationState = NavigationReducer.reduce(action, state),
globalState = globalReducer(action, state),
walletState = WalletReducer.reduce(action, state),
sendState = SendScreenReducer.reduce(action, state.sendState),
detailsState = DetailsReducer.reduce(action, state)
)
}

View file

@ -1,8 +1,11 @@
package com.tangem.tap.common.redux
import com.tangem.tap.common.redux.global.GlobalState
import com.tangem.tap.common.redux.global.globalMiddleware
import com.tangem.tap.common.redux.navigation.NavigationState
import com.tangem.tap.common.redux.navigation.navigationMiddleware
import com.tangem.tap.features.details.redux.DetailsMiddleware
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.features.home.redux.homeMiddleware
import com.tangem.tap.features.send.redux.middlewares.sendMiddleware
import com.tangem.tap.features.send.redux.states.SendState
@ -12,17 +15,19 @@ import org.rekotlin.Middleware
import org.rekotlin.StateType
data class AppState(
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val walletState: WalletState = WalletState(),
val sendState: SendState = SendState(),
val navigationState: NavigationState = NavigationState(),
val globalState: GlobalState = GlobalState(),
val walletState: WalletState = WalletState(),
val sendState: SendState = SendState(),
val detailsState: DetailsState = DetailsState()
) : StateType {
companion object {
fun getMiddleware(): List<Middleware<AppState>> {
return listOf(
logMiddleware, navigationMiddleware, notificationsMiddleware,
homeMiddleware, walletMiddleware, sendMiddleware
logMiddleware, navigationMiddleware, notificationsMiddleware, globalMiddleware,
homeMiddleware, walletMiddleware, sendMiddleware,
DetailsMiddleware().detailsMiddleware
)
}
}

View file

@ -7,5 +7,11 @@ import java.math.BigDecimal
sealed class GlobalAction : Action {
data class SaveScanNoteResponse(val scanNoteResponse: ScanNoteResponse) : GlobalAction()
data class SetFiatRate(val fiatRates: Pair<String, BigDecimal>) : GlobalAction()
data class SetFiatRate(
val fiatRates: Pair<CryptoCurrencyName, BigDecimal>
) : GlobalAction()
data class ChangeAppCurrency(val appCurrency: FiatCurrencyName) : GlobalAction()
object RestoreAppCurrency : GlobalAction() {
data class Success(val appCurrency: FiatCurrencyName) : GlobalAction()
}
}

View file

@ -0,0 +1,21 @@
package com.tangem.tap.common.redux.global
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.preferencesStorage
import com.tangem.tap.store
import org.rekotlin.Middleware
val globalMiddleware: Middleware<AppState> = { dispatch, appState ->
{ nextDispatch ->
{ action ->
when (action) {
is GlobalAction.RestoreAppCurrency -> {
store.dispatch(GlobalAction.RestoreAppCurrency.Success(
preferencesStorage.getAppCurrency()
))
}
}
nextDispatch(action)
}
}
}

View file

@ -13,9 +13,15 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.SaveScanNoteResponse ->
newState = newState.copy(scanNoteResponse = action.scanNoteResponse)
is GlobalAction.SetFiatRate -> {
val rates = newState.fiatRates.rates.toMutableMap()
val rates = newState.conversionRates.rates.toMutableMap()
rates[action.fiatRates.first] = action.fiatRates.second
newState = newState.copy(fiatRates = FiatRates(rates))
newState = newState.copy(conversionRates = ConversionRates(rates))
}
is GlobalAction.ChangeAppCurrency -> {
newState = newState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf()))
}
is GlobalAction.RestoreAppCurrency.Success -> {
newState = newState.copy(appCurrency = action.appCurrency, conversionRates = ConversionRates(mapOf()))
}
}
return newState

View file

@ -1,22 +1,33 @@
package com.tangem.tap.common.redux.global
import com.tangem.commands.common.network.TangemService
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.domain.PayIdManager
import com.tangem.tap.domain.TapWalletManager
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import org.rekotlin.StateType
import java.math.BigDecimal
data class GlobalState(
val scanNoteResponse: ScanNoteResponse? = null,
val tapWalletManager: TapWalletManager = TapWalletManager(),
val fiatRates: FiatRates = FiatRates(emptyMap()),
val payIdManager: PayIdManager = PayIdManager(),
val coinMarketCapService: CoinMarketCapService = CoinMarketCapService(),
val tangemService: TangemService = TangemService(),
val conversionRates: ConversionRates = ConversionRates(emptyMap()),
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY
) : StateType
data class FiatRates(
val rates: Map<String, BigDecimal>
data class ConversionRates(
val rates: Map<CryptoCurrencyName, BigDecimal>,
) {
fun getRateForCryptoCurrency(currency: String): BigDecimal? {
return rates[currency]
fun getRate(cryptoCurrency: CryptoCurrencyName): BigDecimal? {
return rates[cryptoCurrency]
}
}
typealias CryptoCurrencyName = String
typealias FiatCurrencyName = String

View file

@ -9,4 +9,4 @@ data class NavigationState(
val activity: WeakReference<FragmentActivity>? = null
) : StateType
enum class AppScreen { Home, Wallet, Send }
enum class AppScreen { Home, Wallet, Send, Details, DetailsConfirm }

View file

@ -3,6 +3,8 @@ package com.tangem.tap.domain
import androidx.activity.ComponentActivity
import com.tangem.*
import com.tangem.commands.CommandResponse
import com.tangem.commands.PurgeWalletCommand
import com.tangem.commands.PurgeWalletResponse
import com.tangem.common.CompletionResult
import com.tangem.common.extensions.CardType
import com.tangem.tangem_sdk_new.extensions.init
@ -28,6 +30,10 @@ class TangemSdkManager(val activity: ComponentActivity) {
return runTaskAsyncReturnOnMain(CreateWalletAndRescanTask())
}
suspend fun eraseWallet(): CompletionResult<PurgeWalletResponse> {
return runTaskAsyncReturnOnMain(PurgeWalletCommand())
}
private suspend fun <T : CommandResponse> runTaskAsync(
runnable: CardSessionRunnable<T>, cardId: String? = null, initialMessage: Message? = null
): CompletionResult<T> =

View file

@ -8,6 +8,8 @@ import com.tangem.commands.common.network.Result
import com.tangem.commands.common.network.TangemService
import com.tangem.common.extensions.toHexString
import com.tangem.tap.TapConfig
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.domain.tasks.ScanNoteResponse
import com.tangem.tap.features.wallet.redux.PayIdState
@ -54,15 +56,15 @@ class TapWalletManager {
result?.let { handlePayIdResult(it) }
}
suspend fun loadFiatRate() {
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName) {
val wallet = store.state.globalState.scanNoteResponse?.walletManager?.wallet
val blockchainCurrency = wallet?.blockchain?.currency
val tokenCurrency = wallet?.token?.symbol
val blockchainRate = blockchainCurrency?.let { coinMarketCapService.getRate(it) }
val tokenRate = tokenCurrency?.let { coinMarketCapService.getRate(it) }
val blockchainRate = blockchainCurrency?.let { coinMarketCapService.getRate(it, fiatCurrency) }
val tokenRate = tokenCurrency?.let { coinMarketCapService.getRate(it, fiatCurrency) }
val results = mutableListOf<Pair<String, Result<BigDecimal>?>>()
val results = mutableListOf<Pair<CryptoCurrencyName, Result<BigDecimal>?>>()
if (blockchainCurrency != null) results.add(blockchainCurrency to blockchainRate)
if (tokenCurrency != null) results.add(tokenCurrency to tokenRate)
@ -83,7 +85,7 @@ class TapWalletManager {
store.dispatch(WalletAction.LoadWallet)
store.dispatch(WalletAction.LoadFiatRate)
store.dispatch(WalletAction.LoadPayId)
} else if (data.card.status == CardStatus.Empty){
} else if (data.card.status == CardStatus.Empty) {
store.dispatch(WalletAction.EmptyWallet)
} else {
store.dispatch(WalletAction.LoadData.Failure(TapError.UnknownBlockchain))
@ -153,7 +155,7 @@ class TapWalletManager {
}
}
private suspend fun handleFiatRatesResult(results: List<Pair<String, Result<BigDecimal>?>>) {
private suspend fun handleFiatRatesResult(results: List<Pair<CryptoCurrencyName, Result<BigDecimal>?>>) {
withContext(Dispatchers.Main) {
results.map {
when (it.second) {

View file

@ -0,0 +1,44 @@
package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.Card
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.wallet.R
import org.rekotlin.Action
sealed class DetailsAction : Action {
data class PrepareScreen(
val card: Card,
val wallet: Wallet?,
val fiatCurrencyName: FiatCurrencyName,
val fiatCurrencies: List<FiatCurrencyName>? = null,
): DetailsAction()
sealed class EraseWallet : DetailsAction() {
object Check : EraseWallet()
object Proceed : EraseWallet() {
object NotAllowedByCard: EraseWallet(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_allowed
}
object NotEmpty: EraseWallet(), NotificationAction {
override val messageResource = R.string.details_notification_erase_wallet_not_possible
}
}
object Confirm : EraseWallet()
object Cancel : EraseWallet()
object Failure : EraseWallet()
object Success : EraseWallet()
}
sealed class AppCurrencyAction : DetailsAction() {
data class SetCurrencies(val currencies: List<FiatCurrency>) : AppCurrencyAction()
object ChooseAppCurrency : AppCurrencyAction()
object Cancel: AppCurrencyAction()
data class SelectAppCurrency(val fiatCurrencyName: FiatCurrencyName): AppCurrencyAction()
}
}

View file

@ -0,0 +1,81 @@
package com.tangem.tap.features.details.redux
import com.tangem.commands.common.network.Result
import com.tangem.common.CompletionResult
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.features.wallet.redux.WalletAction
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.preferencesStorage
import com.tangem.tap.scope
import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.rekotlin.Middleware
class DetailsMiddleware {
val detailsMiddleware: Middleware<AppState> = { dispatch, state ->
{ next ->
{ action ->
when (action) {
is DetailsAction.PrepareScreen -> {
scope.launch {
val loadedCurrencies = preferencesStorage.getFiatCurrencies()
if (loadedCurrencies.isNullOrEmpty()) {
val response = CoinMarketCapService().getFiatCurrencies()
withContext(Dispatchers.Main) {
when (response) {
is Result.Success -> {
preferencesStorage.saveFiatCurrencies(response.data)
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(response.data))
}
}
}
} else {
withContext(Dispatchers.Main) {
store.dispatch(DetailsAction.AppCurrencyAction.SetCurrencies(loadedCurrencies))
}
}
}
}
is DetailsAction.EraseWallet.Proceed -> {
when (store.state.detailsState.eraseWalletState) {
EraseWalletState.Allowed ->
store.dispatch(NavigationAction.NavigateTo(AppScreen.DetailsConfirm))
EraseWalletState.NotAllowedByCard ->
store.dispatch(DetailsAction.EraseWallet.Proceed.NotAllowedByCard)
EraseWalletState.NotEmpty ->
store.dispatch(DetailsAction.EraseWallet.Proceed.NotEmpty)
}
}
is DetailsAction.EraseWallet.Cancel -> {
store.dispatch(NavigationAction.PopBackTo())
}
is DetailsAction.EraseWallet.Confirm -> {
scope.launch {
val result = tangemSdkManager.eraseWallet()
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
store.dispatch(NavigationAction.PopBackTo(AppScreen.Home))
}
}
}
}
}
is DetailsAction.AppCurrencyAction.SelectAppCurrency -> {
preferencesStorage.saveAppCurrency(action.fiatCurrencyName)
store.dispatch(GlobalAction.ChangeAppCurrency(action.fiatCurrencyName))
store.dispatch(WalletAction.LoadFiatRate)
}
}
next(action)
}
}
}
}

View file

@ -0,0 +1,96 @@
package com.tangem.tap.features.details.redux
import com.tangem.commands.Card
import com.tangem.commands.Settings
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Action
class DetailsReducer {
companion object {
fun reduce(action: Action, state: AppState): DetailsState = internalReduce(action, state)
}
}
private fun internalReduce(action: Action, state: AppState): DetailsState {
if (action !is DetailsAction) return state.detailsState
var detailsState = state.detailsState
when (action) {
is DetailsAction.PrepareScreen -> {
detailsState = DetailsState(
card = action.card, wallet = action.wallet,
cardInfo = action.card.toCardInfo(),
appCurrencyState = AppCurrencyState(
action.fiatCurrencyName
)
)
}
is DetailsAction.EraseWallet -> {
detailsState = handleEraseWallet(action, detailsState)
}
is DetailsAction.AppCurrencyAction -> {
detailsState = handleAppCurrencyAction(action, detailsState)
}
}
return detailsState
}
private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsState): DetailsState {
return when (action) {
DetailsAction.EraseWallet.Check -> {
val notAllowedByCard = state.card?.settingsMask?.contains(Settings.ProhibitPurgeWallet) == true
val notEmpty = state.wallet?.transactions?.isNullOrEmpty() != true ||
state.wallet.amounts.toList().unzip().second.map { it.value?.isZero() }.contains(false)
val eraseWalletState = when {
notAllowedByCard -> EraseWalletState.NotAllowedByCard
notEmpty -> EraseWalletState.NotEmpty
else -> EraseWalletState.Allowed
}
state.copy(eraseWalletState = eraseWalletState)
}
DetailsAction.EraseWallet.Proceed -> {
if (state.eraseWalletState == EraseWalletState.Allowed) {
state.copy(confirmScreenState = ConfirmScreenState.EraseWallet)
} else {
state
}
}
DetailsAction.EraseWallet.Cancel -> state.copy(eraseWalletState = null)
DetailsAction.EraseWallet.Failure -> state.copy(eraseWalletState = null)
DetailsAction.EraseWallet.Success -> state.copy(eraseWalletState = null)
else -> state
}
}
private fun handleAppCurrencyAction(
action: DetailsAction.AppCurrencyAction, state: DetailsState
): DetailsState {
return when (action) {
is DetailsAction.AppCurrencyAction.SetCurrencies -> {
state.copy(appCurrencyState = state.appCurrencyState.copy(fiatCurrencies = action.currencies))
}
DetailsAction.AppCurrencyAction.ChooseAppCurrency -> {
state.copy(appCurrencyState = state.appCurrencyState.copy(showAppCurrencyDialog = true))
}
DetailsAction.AppCurrencyAction.Cancel -> {
state.copy(appCurrencyState = state.appCurrencyState.copy(showAppCurrencyDialog = false))
}
is DetailsAction.AppCurrencyAction.SelectAppCurrency -> {
state.copy(
appCurrencyState = state.appCurrencyState.copy(
fiatCurrencyName = action.fiatCurrencyName, showAppCurrencyDialog = false
)
)
}
else -> state
}
}
private fun Card.toCardInfo(): CardInfo? {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.cardData?.issuerName ?: return null
val signedHashes = this.walletSignedHashes ?: return null
return CardInfo(cardId, issuer, signedHashes)
}

View file

@ -0,0 +1,31 @@
package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
import com.tangem.commands.Card
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import org.rekotlin.StateType
data class DetailsState(
val card: Card? = null,
val wallet: Wallet? = null,
val cardInfo: CardInfo? = null,
val appCurrencyState: AppCurrencyState = AppCurrencyState(),
val eraseWalletState: EraseWalletState? = null,
val confirmScreenState: ConfirmScreenState? = null,
) : StateType
data class CardInfo(
val cardId: String,
val issuer: String,
val signedHashes: Int
)
enum class EraseWalletState { Allowed, NotAllowedByCard, NotEmpty }
enum class ConfirmScreenState { EraseWallet, LongTap, AccessCode, PassCode }
data class AppCurrencyState(
val fiatCurrencyName: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val showAppCurrencyDialog: Boolean = false,
val fiatCurrencies: List<FiatCurrency>? = null,
)

View file

@ -0,0 +1,45 @@
package com.tangem.tap.features.details.ui
import android.content.Context
import androidx.appcompat.app.AlertDialog
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.tangem.tap.common.extensions.toFormattedString
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.tap.store
import com.tangem.wallet.R
class CurrencySelectionDialog {
var dialog: AlertDialog? = null
fun show(currencies: List<FiatCurrency>, currentAppCurrency: FiatCurrencyName, context: Context) {
if (dialog == null) {
val currenciesToShow = currencies.map { it.toFormattedString() }.toTypedArray()
var currentSelection = currencies.indexOfFirst { it.symbol == currentAppCurrency }
dialog = MaterialAlertDialogBuilder(context)
.setTitle(context.getString(R.string.details_currency))
.setNeutralButton(context.getString(R.string.generic_cancel)) { _, _ ->
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)
}
.setPositiveButton(context.getString(R.string.generic_done)) { _, _ ->
val selectedCurrency = currencies[currentSelection]
store.dispatch(DetailsAction.AppCurrencyAction.SelectAppCurrency(selectedCurrency.symbol))
}
.setOnDismissListener {
store.dispatch(DetailsAction.AppCurrencyAction.Cancel)
}
.setSingleChoiceItems(currenciesToShow, currentSelection) { _, which ->
currentSelection = which
}.show()
}
}
fun clear() {
dialog = null
}
}

View file

@ -0,0 +1,77 @@
package com.tangem.tap.features.details.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.extensions.getDrawable
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.ConfirmScreenState
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details_confirm.*
import org.rekotlin.StoreSubscriber
class DetailsConfirmFragment : Fragment(R.layout.fragment_details_confirm),
StoreSubscriber<DetailsState> {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: DetailsState) {
if (activity == null) return
when (state.confirmScreenState) {
ConfirmScreenState.EraseWallet -> {
toolbar.title = getString(R.string.details_erase_wallet)
btn_confirm.text = getString(R.string.details_erase_wallet)
btn_confirm.setCompoundDrawablesRelativeWithIntrinsicBounds(
null, null, getDrawable(R.drawable.ic_send), null
)
btn_confirm.setOnClickListener { store.dispatch(DetailsAction.EraseWallet.Confirm) }
}
ConfirmScreenState.LongTap -> TODO()
ConfirmScreenState.AccessCode -> TODO()
ConfirmScreenState.PassCode -> TODO()
null -> TODO()
}
}
}

View file

@ -0,0 +1,90 @@
package com.tangem.tap.features.details.ui
import android.os.Bundle
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.fragment.app.Fragment
import androidx.transition.TransitionInflater
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_details.*
import kotlinx.android.synthetic.main.fragment_wallet.toolbar
import org.rekotlin.StoreSubscriber
class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<DetailsState> {
private var currencySelectionDialog = CurrencySelectionDialog()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
}
override fun onStart() {
super.onStart()
store.subscribe(this) { state ->
state.skipRepeats { oldState, newState ->
oldState.detailsState == newState.detailsState
}.select { it.detailsState }
}
}
override fun onStop() {
super.onStop()
store.unsubscribe(this)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
}
}
override fun newState(state: DetailsState) {
if (activity == null) return
if (state.cardInfo != null) {
tv_card_id.text = state.cardInfo.cardId
tv_issuer.text = state.cardInfo.issuer
tv_signed_hashes.text = state.cardInfo.signedHashes.toString()
}
tv_erase_wallet.setOnClickListener {
store.dispatch(DetailsAction.EraseWallet.Check)
store.dispatch(DetailsAction.EraseWallet.Proceed)
}
tv_app_currency.text = state.appCurrencyState.fiatCurrencyName
tv_app_currency_title.setOnClickListener {
store.dispatch(DetailsAction.AppCurrencyAction.ChooseAppCurrency)
}
if (state.appCurrencyState.showAppCurrencyDialog &&
!state.appCurrencyState.fiatCurrencies.isNullOrEmpty()) {
currencySelectionDialog.show(
state.appCurrencyState.fiatCurrencies,
state.appCurrencyState.fiatCurrencyName,
requireContext()
)
} else {
currencySelectionDialog.clear()
}
}
}

View file

@ -5,6 +5,7 @@ import android.net.Uri
import androidx.core.content.ContextCompat.startActivity
import com.tangem.common.CompletionResult
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.scope
@ -25,6 +26,7 @@ val homeMiddleware: Middleware<AppState> = { dispatch, state ->
withContext(Dispatchers.Main) {
when (result) {
is CompletionResult.Success -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.state.globalState.tapWalletManager.onCardScanned(result.data)
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
}

View file

@ -2,7 +2,6 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.TapCurrency
import com.tangem.tap.common.extensions.isNegative
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.AmountAction
@ -13,6 +12,7 @@ import com.tangem.tap.features.send.redux.states.AmountState
import com.tangem.tap.features.send.redux.states.MainCurrencyType
import com.tangem.tap.features.send.redux.states.SendState
import com.tangem.tap.features.send.redux.states.Value
import com.tangem.tap.store
import java.math.BigDecimal
/**
@ -42,7 +42,7 @@ class AmountReducer : SendInternalReducer {
state.copy(
viewAmountValue = fiatToSend.stripZeroPlainString(),
viewBalanceValue = converter.toFiat(state.balanceCrypto).stripZeroPlainString(),
mainCurrency = Value(MainCurrencyType.FIAT, TapCurrency.main),
mainCurrency = Value(MainCurrencyType.FIAT, store.state.globalState.appCurrency),
maxLengthOfAmount = sendState.getDecimals(action.mainCurrency),
cursorAtTheSamePosition = false
)

View file

@ -3,11 +3,11 @@ package com.tangem.tap.features.send.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.CurrencyConverter
import com.tangem.tap.common.entities.TapCurrency
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.features.send.redux.ReceiptAction.RefreshReceipt
import com.tangem.tap.features.send.redux.SendScreenAction
import com.tangem.tap.features.send.redux.states.*
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
@ -163,7 +163,7 @@ class ReceiptReducer : SendInternalReducer {
private fun determineSymbols(wallet: Wallet): ReceiptSymbols {
return ReceiptSymbols(
fiat = TapCurrency.main,
fiat = store.state.globalState.appCurrency,
crypto = wallet.blockchain.currency,
token = wallet.amounts[AmountType.Token]?.currencySymbol
)

View file

@ -65,7 +65,7 @@ private class PrepareSendScreenStatesReducer : SendInternalReducer {
}
private fun createCurrencyConverter(walletManager: WalletManager): CurrencyConverter {
val rate = store.state.globalState.fiatRates.getRateForCryptoCurrency(walletManager.wallet.blockchain.currency)
val rate = store.state.globalState.conversionRates.getRate(walletManager.wallet.blockchain.currency)
return if (rate == null) CurrencyConverter(BigDecimal.ONE) else CurrencyConverter(rate)
}
}

View file

@ -60,7 +60,7 @@ enum class SendButtonState {
data class AmountState(
val viewAmountValue: String = BigDecimal.ZERO.toPlainString(),
val viewBalanceValue: String = BigDecimal.ZERO.toPlainString(),
val mainCurrency: Value<MainCurrencyType> = Value(MainCurrencyType.FIAT, TapCurrency.main),
val mainCurrency: Value<MainCurrencyType> = Value(MainCurrencyType.FIAT, TapCurrency.DEFAULT_FIAT_CURRENCY),
val typeOfAmount: AmountType = AmountType.Coin,
val amountToSendCrypto: BigDecimal = BigDecimal.ZERO,
val balanceCrypto: BigDecimal = BigDecimal.ZERO,

View file

@ -197,7 +197,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
private fun restoreMainCurrency(): MainCurrencyType {
val sp = requireContext().getSharedPreferences("SendScreen", Context.MODE_PRIVATE)
val mainCurrency = sp.getString("mainCurrency", TapCurrency.main)
val mainCurrency = sp.getString("mainCurrency", TapCurrency.DEFAULT_FIAT_CURRENCY)
val foundType = MainCurrencyType.values()
.firstOrNull { it.name.toLowerCase() == mainCurrency!!.toLowerCase() } ?: MainCurrencyType.FIAT
return foundType

View file

@ -6,6 +6,7 @@ import com.tangem.blockchain.common.Wallet
import com.tangem.commands.Card
import com.tangem.tap.common.redux.ErrorAction
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.TapError
import com.tangem.wallet.R
import org.rekotlin.Action
@ -24,7 +25,7 @@ sealed class WalletAction : Action {
}
object LoadFiatRate : WalletAction() {
data class Success(val fiatRates: Pair<String, BigDecimal>) : WalletAction()
data class Success(val fiatRates: Pair<CryptoCurrencyName, BigDecimal?>) : WalletAction()
object Failure : WalletAction()
}

View file

@ -42,7 +42,7 @@ val walletMiddleware: Middleware<AppState> = { dispatch, state ->
}
is WalletAction.LoadFiatRate -> {
scope.launch {
store.state.globalState.tapWalletManager.loadFiatRate()
store.state.globalState.tapWalletManager.loadFiatRate(store.state.globalState.appCurrency)
}
}
is WalletAction.LoadArtwork -> {

View file

@ -86,10 +86,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
)
}
is WalletAction.LoadWallet.Success -> {
val fiatCurrencySymbol = state.globalState.appCurrency
val token = action.wallet.amounts[AmountType.Token]
val tokenData = if (token != null) {
val tokenFiatRate = state.globalState.fiatRates.getRateForCryptoCurrency(token.currencySymbol)
val tokenFiatAmount = tokenFiatRate?.let { token.value?.toFiatString(it) }
val tokenFiatRate = state.globalState.conversionRates.getRate(token.currencySymbol)
val tokenFiatAmount = tokenFiatRate?.let { token.value?.toFiatString(it, fiatCurrencySymbol) }
TokenData(
token.value?.toFormattedString(token.decimals) ?: "",
token.currencySymbol, tokenFiatAmount)
@ -97,8 +98,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
null
}
val amount = action.wallet.amounts[AmountType.Coin]?.value
val fiatRate = state.globalState.fiatRates.getRateForCryptoCurrency(action.wallet.blockchain.currency)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it) }
val fiatRate = state.globalState.conversionRates.getRate(action.wallet.blockchain.currency)
val fiatAmount = fiatRate?.let { amount?.toFiatString(it, fiatCurrencySymbol) }
val pendingTransactions = action.wallet.transactions
.toPendingTransactions(action.wallet.address)
@ -135,16 +136,24 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
errorMessage = action.errorMessage
)
)
is WalletAction.LoadFiatRate -> {
newState.copy(currencyData = newState.currencyData.copy(
fiatAmount = null,
token = newState.currencyData.token?.copy(fiatAmount = null))
)
}
is WalletAction.LoadFiatRate.Success -> {
val rate = action.fiatRates.second
val rate = action.fiatRates.second ?: return newState
val currency = action.fiatRates.first
val fiatAmount = if (currency == newState.wallet?.blockchain?.currency) {
newState.wallet?.amounts?.get(AmountType.Coin)?.value?.toFiatString(rate)
newState.wallet?.amounts?.get(AmountType.Coin)?.value
?.toFiatString(rate, state.globalState.appCurrency)
} else {
newState.currencyData.fiatAmount
}
val tokenFiatAmount = if (currency == newState.wallet?.token?.symbol) {
newState.wallet?.amounts?.get(AmountType.Token)?.value?.toFiatString(rate)
newState.wallet?.amounts?.get(AmountType.Token)?.value
?.toFiatString(rate, state.globalState.appCurrency)
} else {
newState.currencyData.token?.fiatAmount
}

View file

@ -3,8 +3,12 @@ package com.tangem.tap.features.wallet.ui
import android.app.Dialog
import android.graphics.Bitmap
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.transition.TransitionInflater
@ -12,7 +16,9 @@ import com.google.android.material.snackbar.Snackbar
import com.tangem.tap.common.extensions.getDrawable
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.dialogs.AmountToSendDialog
import com.tangem.tap.features.wallet.ui.dialogs.PayIdDialog
@ -35,6 +41,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
activity?.onBackPressedDispatcher?.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
store.dispatch(NavigationAction.PopBackTo())
@ -61,6 +68,7 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
(activity as? AppCompatActivity)?.setSupportActionBar(toolbar)
toolbar.setNavigationOnClickListener {
store.dispatch(NavigationAction.PopBackTo())
@ -148,10 +156,10 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
is WalletMainButton.SendButton -> R.string.wallet_button_send
is WalletMainButton.CreateWalletButton -> R.string.wallet_button_create_wallet
}
btn_main.text = getString(buttonTitle)
btn_main.isEnabled = state.mainButton.enabled
btn_confirm.text = getString(buttonTitle)
btn_confirm.isEnabled = state.mainButton.enabled
btn_main.setOnClickListener {
btn_confirm.setOnClickListener {
when (state.mainButton) {
is WalletMainButton.SendButton -> store.dispatch(WalletAction.Send())
is WalletMainButton.CreateWalletButton -> store.dispatch(WalletAction.CreateWallet)
@ -211,4 +219,25 @@ class WalletFragment : Fragment(R.layout.fragment_wallet), StoreSubscriber<Walle
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.details_menu -> {
store.state.globalState.scanNoteResponse?.card?.let { card ->
store.dispatch(DetailsAction.PrepareScreen(
card, store.state.walletState.wallet,
store.state.globalState.appCurrency
))
store.dispatch(NavigationAction.NavigateTo(AppScreen.Details))
true
}
false
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.wallet, menu)
}
}

View file

@ -12,9 +12,14 @@ interface CoinMarketCapApi {
@GET("v1/tools/price-conversion")
suspend fun getRateInfo(
@Query("amount") amount: Int,
@Query("symbol") cryptoId: String
@Query("symbol") cryptoCurrencyName: String,
@Query("convert") fiatCurrencyName: String? = null
): RateInfoResponse
@GET("v1/fiat/map")
suspend fun getFiatMap(): FiatMapResponse
companion object {
private const val baseUrl = "https://pro-api.coinmarketcap.com/"

View file

@ -2,6 +2,7 @@ package com.tangem.tap.network.coinmarketcap
import com.tangem.commands.common.network.Result
import com.tangem.commands.common.network.performRequest
import com.tangem.tap.common.redux.global.FiatCurrencyName
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.math.BigDecimal
@ -9,10 +10,20 @@ import java.math.BigDecimal
class CoinMarketCapService {
private val api: CoinMarketCapApi by lazy { CoinMarketCapApi.create() }
suspend fun getRate(currency: String): Result<BigDecimal> = withContext(Dispatchers.IO) {
val response = performRequest { api.getRateInfo(1, currency) }
suspend fun getRate(
currency: String, fiatCurrency: FiatCurrencyName? = null
): Result<BigDecimal> = withContext(Dispatchers.IO) {
val response = performRequest { api.getRateInfo(1, currency, fiatCurrency) }
return@withContext when (response) {
is Result.Success -> Result.Success(response.data.data.quote.usd.price)
is Result.Success -> Result.Success(response.data.data.getRate())
is Result.Failure -> response
}
}
suspend fun getFiatCurrencies(): Result<List<FiatCurrency>> = withContext(Dispatchers.IO) {
val response = performRequest { api.getFiatMap() }
return@withContext when (response) {
is Result.Success -> Result.Success(response.data.data.sortedBy { it.name })
is Result.Failure -> response
}
}

View file

@ -5,21 +5,14 @@ import com.squareup.moshi.JsonClass
import java.math.BigDecimal
@JsonClass(generateAdapter = true)
data class RateInfoResponse(
val status: Status,
val data: RateData
)
class RateInfoResponse : CoinMarketResponse<RateData>()
@JsonClass(generateAdapter = true)
data class RateData(
val quote: Quote
)
@JsonClass(generateAdapter = true)
data class Quote(
@Json(name = "USD")
val usd: CurrencyRate
)
val quote: Map<String, CurrencyRate>
) {
fun getRate(): BigDecimal = quote.values.first().price
}
@JsonClass(generateAdapter = true)
data class CurrencyRate(
@ -38,4 +31,21 @@ data class Status(
@Json(name = "credit_count")
val creditCount: Int,
val notice: String?
)
)
@JsonClass(generateAdapter = true)
class FiatMapResponse : CoinMarketResponse<List<FiatCurrency>>()
@JsonClass(generateAdapter = true)
open class CoinMarketResponse<T : Any> {
lateinit var status: Status
lateinit var data: T
}
@JsonClass(generateAdapter = true)
data class FiatCurrency(
val id: Int,
val name: String,
val sign: String,
val symbol: String
)

View file

@ -0,0 +1,54 @@
package com.tangem.tap.persistence
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.Moshi
import com.squareup.moshi.Types
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.network.coinmarketcap.FiatCurrency
class PreferencesStorage(applicationContext: Application) {
private val preferences: SharedPreferences by lazy {
applicationContext.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE)
}
private val fiatCurrenciesAdapter: JsonAdapter<List<FiatCurrency>> by lazy {
val moshi = Moshi.Builder()
.add(KotlinJsonAdapterFactory())
.build()
val type = Types.newParameterizedType(List::class.java, FiatCurrency::class.java)
moshi.adapter(type)
}
fun getAppCurrency(): FiatCurrencyName {
return preferences.getString(APP_CURRENCY_KEY, DEFAULT_FIAT_CURRENCY)
?: DEFAULT_FIAT_CURRENCY
}
fun saveAppCurrency(fiatCurrencyName: FiatCurrencyName) {
return preferences.edit().putString(APP_CURRENCY_KEY, fiatCurrencyName).apply()
}
fun getFiatCurrencies(): List<FiatCurrency>? {
val json = preferences.getString(FIAT_CURRENCIES_KEY, "")
return if (json.isNullOrBlank()) null else fiatCurrenciesAdapter.fromJson(json) as List<FiatCurrency>
}
fun saveFiatCurrencies(currencies: List<FiatCurrency>) {
val json: String = fiatCurrenciesAdapter.toJson(currencies)
return preferences.edit().putString(FIAT_CURRENCIES_KEY, json).apply()
}
companion object {
private const val PREFERENCES_NAME = "tapPrefs"
private const val APP_CURRENCY_KEY = "appCurrency"
private const val FIAT_CURRENCIES_KEY = "fiatCurrencies"
}
}

View file

@ -0,0 +1,240 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:id="@+id/coordinator_wallet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/details_toolbar_title" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/sv_wallet"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="35dp"
android:fillViewport="true"
android:overScrollMode="never"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="33dp">
<TextView
android:id="@+id/tv_card_id_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:text="@string/details_card_id"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_card_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="0000 0000 0000 0000" />
<TextView
android:id="@+id/tv_issuer_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:text="@string/details_issuer"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_card_id_title" />
<TextView
android:id="@+id/tv_issuer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="14dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_card_id"
tools:text="Tangem" />
<TextView
android:id="@+id/tv_signed_hashes_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/details_signed_hashes"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_issuer_title" />
<TextView
android:id="@+id/tv_signed_hashes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_issuer"
tools:text="48 hashes" />
<TextView
android:id="@+id/tv_settings_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="40dp"
android:paddingBottom="4dp"
android:text="@string/details_settings"
android:textAllCaps="true"
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/separatorGrey2"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title" />
<TextView
android:id="@+id/tv_app_currency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:drawablePadding="15dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title"
tools:text="USD" />
<TextView
android:id="@+id/tv_app_currency_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:text="@string/details_currency"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title" />
<!-- <androidx.constraintlayout.widget.Group-->
<!-- android:id="@+id/group_app_currency"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- app:constraint_referenced_ids="tv_app_currency_title, tv_app_currency" />-->
<TextView
android:id="@+id/tv_card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:paddingBottom="4dp"
android:text="@string/details_card"
android:textAllCaps="true"
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/separatorGrey2"
app:layout_constraintTop_toBottomOf="@id/tv_card_title" />
<TextView
android:id="@+id/tv_validate_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="7dp"
android:drawablePadding="15dp"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_card_title"
android:text="@string/details_validate_card" />
<TextView
android:id="@+id/tv_security"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:drawablePadding="15dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_validate_card"
tools:text="Long Tap" />
<TextView
android:id="@+id/tv_security_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_manage_security"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_validate_card" />
<TextView
android:id="@+id/tv_erase_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="10dp"
android:paddingBottom="7dp"
android:drawablePadding="15dp"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_security_title"
android:text="@string/details_erase_wallet" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,103 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:id="@+id/coordinator_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/backgroundLightGray"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar"
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:fitsSystemWindows="true"
app:liftOnScroll="true">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_details_confirm"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginBottom="33dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<ImageView
android:id="@+id/iv_warning"
android:layout_width="120dp"
android:layout_height="120dp"
android:layout_marginTop="110dp"
android:src="@drawable/ic_warning"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Warning"
android:textAllCaps="true"
android:textSize="32sp"
android:layout_marginTop="32dp"
android:textColor="@color/warning"
app:layout_constraintTop_toBottomOf="@id/iv_warning"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/tv_warning_description"
android:text="Lorem ipsum dolor sit amet, consectetur adipiscing elit. Et quis vitae dictumst consequat."
android:textSize="16sp"
android:layout_marginTop="22dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@id/tv_warning"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<Space
android:id="@+id/space"
android:layout_width="93dp"
android:layout_height="48dp"
android:layout_marginStart="30dp"
android:layout_marginTop="30dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_confirm"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_confirm"
style="@style/TapBlackButton"
android:layout_width="200dp"
android:layout_height="48dp"
android:layout_marginStart="7dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="30dp"
tools:text="@string/wallet_button_send"
tools:icon="@drawable/ic_send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/space"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -20,6 +20,7 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:menu="@menu/wallet"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="@string/wallet_toolbar_title" />
@ -36,8 +37,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
android:overScrollMode="never"
>
android:overScrollMode="never">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_wallet"
@ -59,13 +59,13 @@
app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:layout_marginTop="12dp"
android:id="@+id/rv_pending_transaction"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:nestedScrollingEnabled="false"
android:overScrollMode="never"
app:layout_constraintTop_toBottomOf="@id/iv_card"/>
app:layout_constraintTop_toBottomOf="@id/iv_card" />
<include
android:id="@+id/l_card_balance"
@ -93,7 +93,7 @@
android:text="@string/wallet_button_scan"
app:icon="@drawable/ic_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/btn_main"
app:layout_constraintEnd_toStartOf="@+id/btn_confirm"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
@ -101,7 +101,7 @@
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_main"
android:id="@+id/btn_confirm"
style="@style/TapButtonWithIcon"
android:layout_width="200dp"
android:layout_height="48dp"

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/details_menu"
android:title="@string/details_toolbar_title"
app:showAsAction="never" />
</menu>

View file

@ -3,6 +3,7 @@
<string name="app_name" translatable="false">Tangem Tap</string>
<string name="generic_done">Done</string>
<string name="generic_cancel">Cancel</string>
<string name="generic_retry">Retry</string>
<string name="generic_and">and</string>
@ -74,4 +75,18 @@
<string name="send_set_maximum_amount">Maximum amount</string>
<string name="send_transaction_complete">Transaction was signed and sent to the blockchain</string>
<string name="details_toolbar_title">Details</string>
<string name="details_notification_erase_wallet_not_allowed">Card settings prohibits from erasing wallet</string>
<string name="details_notification_erase_wallet_not_possible">You balance on this wallet is not zero, or you have unconfirmed transactions</string>
<string name="details_card_id">Card ID</string>
<string name="details_issuer">Issuer</string>
<string name="details_signed_hashes">Signed</string>
<string name="details_settings">Settings</string>
<string name="details_currency">App currency</string>
<string name="details_card">Card</string>
<string name="details_validate_card">Validate card</string>
<string name="details_manage_security">Manage security</string>
<string name="details_erase_wallet">Erase wallet</string>
</resources>