Updated on 2026-08-14

This commit is contained in:
Tangem 2021-11-19 22:07:12 +03:00
commit 09065f1672
103 changed files with 1414 additions and 882 deletions

View file

@ -38,6 +38,7 @@ android {
}
debug_beta {
initWith release
debuggable false
versionNameSuffix "-beta"
applicationIdSuffix ".debug"
buildConfigField 'String', 'CONFIG_ENVIRONMENT', '\"prod\"'
@ -71,14 +72,13 @@ dependencies {
implementation 'androidx.fragment:fragment-ktx:1.3.6'
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
implementation 'com.google.android.material:material:1.4.0'
implementation "androidx.core:core-ktx:1.6.0"
implementation 'com.google.android.play:core:1.10.2'
implementation 'com.google.android.play:core-ktx:1.8.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
implementation 'com.tangem:blockchain:develop-42'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-91'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-91'
implementation 'com.tangem:blockchain:develop-48'
implementation 'com.tangem.tangem-sdk-kotlin:core:develop-95'
implementation 'com.tangem.tangem-sdk-kotlin:android:develop-95'
// WebView
implementation "androidx.browser:browser:1.3.0"

View file

@ -19,6 +19,13 @@
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<queries>
<intent>
<action android:name="android.intent.action.SENDTO" />
<data android:scheme="mailto" />
</intent>
</queries>
<application
android:name="com.tangem.tap.TapApplication"
android:allowBackup="true"

@ -1 +1 @@
Subproject commit 0cf3af21e6f441e6323a1ba127ee7518168797df
Subproject commit e80938f7d9ba378a91ac544a9170597e64afa696

View file

@ -62,7 +62,7 @@ class MainActivity : AppCompatActivity(), SnackbarHandler {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
systemActions()
store.state.globalState.feedbackManager?.updateAcivity(this)
store.state.globalState.feedbackManager?.updateActivity(this)
store.dispatch(NavigationAction.ActivityCreated(WeakReference(this)))
tangemSdk = TangemSdk.init(this, TangemSdkManager.config)

View file

@ -43,24 +43,28 @@ class IntentHandler {
}
private fun handleSellCurrencyCallback(intent: Intent?) {
val transactionID =
intent?.data?.getQueryParameter(TradeCryptoHelper.TRANSACTION_ID_PARAM) ?: return
val currency =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_CODE_PARAM) ?: return
val amount =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_AMOUNT_PARAM) ?: return
val destinationAddress =
intent.data?.getQueryParameter(TradeCryptoHelper.DEPOSIT_WALLET_ADDRESS_PARAM)
?: return
try {
val transactionID =
intent?.data?.getQueryParameter(TradeCryptoHelper.TRANSACTION_ID_PARAM) ?: return
val currency =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_CODE_PARAM) ?: return
val amount =
intent.data?.getQueryParameter(TradeCryptoHelper.CURRENCY_AMOUNT_PARAM) ?: return
val destinationAddress =
intent.data?.getQueryParameter(TradeCryptoHelper.DEPOSIT_WALLET_ADDRESS_PARAM)
?: return
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
Timber.d("MoonPay Sell: $amount $currency to $destinationAddress")
store.dispatch(WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID
))
store.dispatch(WalletAction.TradeCryptoAction.SendCrypto(
currencyId = currency,
amount = amount,
destinationAddress = destinationAddress,
transactionId = transactionID
))
} catch (exception: Exception) {
Timber.d("Not Moonpay URL")
}
}

View file

@ -0,0 +1,63 @@
package com.tangem.tap.common.extensions
import android.app.Activity
import android.content.Intent
import android.net.Uri
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import java.io.File
/**
* required since targetAndroid=30
* <queries>
* <intent>
* <action android:name="android.intent.action.SENDTO" />
* <data android:scheme="mailto" />
* </intent>
* </queries>
*/
fun Activity.sendEmail(
email: String,
subject: String,
message: String,
file: File? = null,
onFail: ((Exception) -> Unit)? = null
) {
fun createEmailShareIntent(recipient: String, subject: String, text: String, file: File? = null): Intent {
val builder = ShareCompat.IntentBuilder.from(this)
.setType("message/rfc822")
.setEmailTo(arrayOf(recipient))
.setSubject(subject)
.setText(text)
file?.let { builder.setStream(FileProvider.getUriForFile(this, "${packageName}.provider", it)) }
return builder.intent
}
val originalIntent = createEmailShareIntent(email, subject, message, file)
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
val originalIntentResults = packageManager.queryIntentActivities(originalIntent, 0)
val emailFilterIntentResults = packageManager.queryIntentActivities(emailFilterIntent, 0)
val targetedIntents = originalIntentResults
.filter { originalResult ->
emailFilterIntentResults.any {
originalResult.activityInfo.packageName == it.activityInfo.packageName
}
}
.map {
createEmailShareIntent(email, subject, message, file).apply {
setPackage(it.activityInfo.packageName)
}
}
.toMutableList()
try {
val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ContextCompat.startActivity(this, chooserIntent, null)
} catch (ex: Exception) {
onFail?.invoke(ex)
}
}

View file

@ -15,9 +15,8 @@ fun Picasso.loadCurrenciesIcon(
imageView: ImageView,
textView: TextView,
token: Token? = null,
blockchain: Blockchain?,
blockchain: Blockchain,
) {
val blockchain = blockchain ?: Blockchain.Ethereum
val url = if (token != null) {
IconsUtil.getTokenIconUri(blockchain, token)

View file

@ -18,14 +18,18 @@ fun Store<*>.dispatchOnMain(action: Action) {
}
}
suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse) {
store.state.globalState.tapWalletManager.onCardScanned(scanResponse)
suspend fun Store<*>.onCardScanned(scanResponse: ScanResponse, addAnalytics: Boolean = true) {
store.state.globalState.tapWalletManager.onCardScanned(scanResponse, addAnalytics)
}
fun Store<*>.dispatchOpenUrl(url: String) {
store.dispatch(NavigationAction.OpenUrl(url))
}
fun Store<*>.dispatchShare(url: String) {
store.dispatch(NavigationAction.Share(url))
}
fun Store<*>.dispatchNotification(resId: Int) {
scope.launch(Dispatchers.Main) {
store.dispatch(GlobalAction.ShowNotification(resId))

View file

@ -1,7 +1,7 @@
package com.tangem.tap.common.redux
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.features.wallet.redux.AddressData
import com.tangem.tap.features.wallet.redux.Currency
/**
[REDACTED_AUTHOR]
@ -11,8 +11,7 @@ interface StateDialog
sealed class AppDialog : StateDialog {
object ScanFailsDialog : AppDialog()
data class AddressInfoDialog(
val currency: Currency,
val addressData: AddressData,
val onCopyAddress: VoidCallback,
val onExploreAddress: VoidCallback
) : AppDialog()
}

View file

@ -12,7 +12,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.details.redux.SecurityOption
import com.tangem.tap.features.feedback.EmailData
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import com.tangem.tap.network.moonpay.MoonpayStatus
import org.rekotlin.Action
sealed class GlobalAction : Action {
@ -72,7 +72,7 @@ sealed class GlobalAction : Action {
data class SendFeedback(val emailData: EmailData) : GlobalAction()
data class UpdateFeedbackInfo(val walletManagers: List<WalletManager>) : GlobalAction()
object GetMoonPayUserStatus : GlobalAction() {
data class Success(val moonPayUserStatus: MoonPayUserStatus) : GlobalAction()
object GetMoonPayStatus : GlobalAction() {
data class Success(val moonPayStatus: MoonpayStatus) : GlobalAction()
}
}

View file

@ -19,6 +19,7 @@ import com.tangem.tap.store
import com.tangem.tap.tangemSdkManager
import kotlinx.coroutines.launch
import org.rekotlin.Middleware
import timber.log.Timber
class GlobalMiddleware {
companion object {
@ -74,15 +75,16 @@ private val globalMiddlewareHandler: Middleware<AppState> = { dispatch, appState
store.state.globalState.feedbackManager?.infoHolder
?.setWalletsInfo(action.walletManagers)
}
is GlobalAction.GetMoonPayUserStatus -> {
is GlobalAction.GetMoonPayStatus -> {
val apiKey = appState()?.globalState?.configManager?.config?.moonPayApiKey
if (apiKey != null) {
scope.launch {
val userStatusResponse = MoonpayService().getUserStatus(apiKey)
if (userStatusResponse is Result.Success) {
store.dispatchOnMain(
GlobalAction.GetMoonPayUserStatus.Success(userStatusResponse.data)
)
val result = MoonpayService().getMoonpayStatus(apiKey)
when (result) {
is Result.Success -> {
store.dispatchOnMain(GlobalAction.GetMoonPayStatus.Success(result.data))
}
is Result.Failure -> Timber.e(result.error)
}
}
}

View file

@ -2,6 +2,7 @@ package com.tangem.tap.common.redux.global
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.tap.preferencesStorage
import org.rekotlin.Action
@ -42,20 +43,14 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
}
is GlobalAction.SetWarningManager -> globalState.copy(warningManager = action.warningManager)
is GlobalAction.UpdateWalletSignedHashes -> {
val wallet = globalState.scanResponse?.card
?.wallet(action.walletPublicKey)
?.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
)
val card = globalState.scanResponse?.card
wallet?.let { globalState.scanResponse.card.updateWallet(wallet) }
val card = globalState.scanResponse?.card ?: return globalState
val wallet = card.wallet(action.walletPublicKey) ?: return globalState
if (card != null) {
globalState.copy(scanResponse = globalState.scanResponse.copy(card = card))
} else {
globalState
}
val newCardInstance = card.updateWallet(wallet.copy(
totalSignedHashes = action.walletSignedHashes,
remainingSignatures = action.remainingSignatures
))
globalState.copy(scanResponse = globalState.scanResponse.copy(card = newCardInstance))
}
is GlobalAction.SetFeedbackManager -> {
globalState.copy(feedbackManager = action.feedbackManager)
@ -66,8 +61,19 @@ fun globalReducer(action: Action, state: AppState): GlobalState {
is GlobalAction.HideDialog -> {
globalState.copy(dialog = null)
}
is GlobalAction.GetMoonPayUserStatus.Success -> {
globalState.copy(moonPayUserStatus = action.moonPayUserStatus)
is GlobalAction.GetMoonPayStatus.Success -> {
val fiatExchangeIsEnabled = globalState.configManager?.config?.isTopUpEnabled ?: false
val moonpayStatus = if (fiatExchangeIsEnabled) {
action.moonPayStatus
} else {
MoonpayStatus(
isBuyAllowed = false,
isSellAllowed = false,
availableToBuy = emptyList(),
availableToSell = emptyList()
)
}
globalState.copy(moonpayStatus = moonpayStatus)
}
is GlobalAction.SetIfCardVerifiedOnline ->
globalState.copy(cardVerifiedOnline = action.verified)

View file

@ -10,7 +10,7 @@ import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.feedback.FeedbackManager
import com.tangem.tap.features.onboarding.OnboardingManager
import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.network.moonpay.MoonPayUserStatus
import com.tangem.tap.network.moonpay.MoonpayStatus
import org.rekotlin.StateType
data class GlobalState(
@ -26,7 +26,7 @@ data class GlobalState(
val appCurrency: FiatCurrencyName = DEFAULT_FIAT_CURRENCY,
val scanCardFailsCounter: Int = 0,
val dialog: StateDialog? = null,
val moonPayUserStatus: MoonPayUserStatus? = null,
val moonpayStatus: MoonpayStatus? = null,
val resources: AndroidResources = AndroidResources(),
) : StateType

View file

@ -15,6 +15,8 @@ sealed class NavigationAction : Action {
data class OpenUrl(val url: String) : NavigationAction()
data class Share(val data: String) : NavigationAction()
data class ActivityCreated(val activity: WeakReference<FragmentActivity>) : NavigationAction()
object ActivityDestroyed : NavigationAction()
}

View file

@ -3,6 +3,7 @@ package com.tangem.tap.common.redux.navigation
import com.tangem.tap.common.CustomTabsManager
import com.tangem.tap.common.extensions.openFragment
import com.tangem.tap.common.extensions.popBackTo
import com.tangem.tap.common.extensions.shareText
import com.tangem.tap.common.redux.AppState
import org.rekotlin.Middleware
@ -31,6 +32,11 @@ val navigationMiddleware: Middleware<AppState> = { dispatch, state ->
CustomTabsManager().openUrl(action.url, it)
}
}
is NavigationAction.Share -> {
navState?.activity?.get()?.let {
it.shareText(action.data)
}
}
}
}
next(action)

View file

@ -125,6 +125,8 @@ class TangemSdkManager(private val tangemSdk: TangemSdk, private val context: Co
companion object {
val config = Config(
linkedTerminal = true,
allowUntrustedCards = true,
filter = CardFilter(
allowedCardTypes = FirmwareVersion.FirmwareType.values().toList()
)

View file

@ -24,7 +24,6 @@ import com.tangem.tap.network.coinmarketcap.CoinMarketCapService
import com.tangem.tap.store
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
import java.math.BigDecimal
@ -55,7 +54,6 @@ class TapWalletManager {
}
suspend fun loadFiatRate(fiatCurrency: FiatCurrencyName, wallet: Wallet) {
Timber.d(wallet.getTokens().toString())
val currencies = wallet.getTokens()
.map { Currency.Token(it) }
.plus(Currency.Blockchain(wallet.blockchain))
@ -97,7 +95,7 @@ class TapWalletManager {
}
}
private fun updateConfigManager(data: ScanResponse) {
fun updateConfigManager(data: ScanResponse) {
val configManager = store.state.globalState.configManager
val blockchain = data.getBlockchain()
if (data.card.isStart2Coin) {
@ -121,7 +119,6 @@ class TapWalletManager {
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
val blockchain = data.getBlockchain()
val primaryWalletManager = walletManagerFactory.makePrimaryWalletManager(data)
@ -145,11 +142,8 @@ class TapWalletManager {
loadMultiWalletData(data.card, blockchain, null)
}
}
val moonPayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonPayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonPayUserStatus?.isSellAllowed == true,
))
val moonPayStatus = store.state.globalState.moonpayStatus
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
store.dispatch(WalletAction.LoadFiatRate())
}
}
@ -157,22 +151,22 @@ class TapWalletManager {
private fun loadMultiWalletData(
card: Card, primaryBlockchain: Blockchain?, primaryWalletManager: WalletManager?
) {
val primaryTokens = primaryWalletManager?.cardTokens ?: emptySet()
val primaryTokens = primaryWalletManager?.cardTokens?.toList() ?: emptyList()
val savedCurrencies = currenciesRepository.loadCardCurrencies(card.cardId)
if (savedCurrencies == null) {
if (primaryBlockchain != null && primaryWalletManager != null) {
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(
blockchains = setOf(primaryBlockchain), tokens = primaryTokens
blockchains = listOf(primaryBlockchain), tokens = primaryTokens
)))
store.dispatch(WalletAction.MultiWallet.AddWalletManagers(primaryWalletManager))
store.dispatch(WalletAction.MultiWallet.AddBlockchains(listOf(primaryBlockchain)))
store.dispatch(WalletAction.MultiWallet.AddTokens(primaryTokens.toList()))
} else {
val blockchains = setOf(Blockchain.Bitcoin, Blockchain.Ethereum)
val blockchains = listOf(Blockchain.Bitcoin, Blockchain.Ethereum)
store.dispatch(WalletAction.MultiWallet.SaveCurrencies(
CardCurrencies(blockchains = blockchains, tokens = emptySet())
CardCurrencies(blockchains = blockchains, tokens = emptyList())
))
val walletManagers =
walletManagerFactory.makeWalletManagersForApp(card, blockchains.toList())
@ -211,12 +205,8 @@ class TapWalletManager {
return@withContext
}
val config = store.state.globalState.configManager?.config ?: return@withContext
val moonpayUserStatus = store.state.globalState.moonPayUserStatus
store.dispatch(WalletAction.LoadWallet(
allowToBuy = config.isTopUpEnabled && moonpayUserStatus?.isBuyAllowed == true,
allowToSell = config.isTopUpEnabled && moonpayUserStatus?.isSellAllowed == true,
))
val moonPayStatus = store.state.globalState.moonpayStatus
store.dispatch(WalletAction.LoadWallet(moonPayStatus))
store.dispatch(WalletAction.LoadFiatRate())
}
}

View file

@ -80,6 +80,7 @@ class ConfigManager(
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId
@ -90,6 +91,7 @@ class ConfigManager(
moonPayApiKey = values.moonPayApiKey,
moonPayApiSecretKey = values.moonPayApiSecretKey,
blockchainSdkConfig = BlockchainSdkConfig(
blockchairApiKey = values.blockchairApiKey,
blockchairAuthorizationToken = values.blockchairAuthorizationToken,
blockcypherTokens = values.blockcypherTokens,
infuraProjectId = values.infuraProjectId

View file

@ -15,6 +15,7 @@ class ConfigValueModel(
val coinMarketCapKey: String,
val moonPayApiKey: String,
val moonPayApiSecretKey: String,
val blockchairApiKey: String?,
val blockchairAuthorizationToken: String?,
val blockcypherTokens: Set<String>?,
val infuraProjectId: String?,

View file

@ -15,7 +15,7 @@ fun Blockchain.isNoAccountError(exception: Throwable): Boolean {
fun Blockchain.amountToCreateAccount(token: Token? = null): BigDecimal? {
return when (this) {
Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(1.5) else BigDecimal.ZERO
Blockchain.Stellar -> if (token?.symbol == NODL) BigDecimal(1.5) else BigDecimal.ONE
Blockchain.XRP -> BigDecimal(10)
else -> null
}

View file

@ -0,0 +1,41 @@
package com.tangem.tap.domain.extensions
import com.tangem.blockchain.common.Blockchain
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.tap.store
/**
[REDACTED_AUTHOR]
*/
fun MoonpayStatus.buyIsAllowed(currency: Currency): Boolean {
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
if (!isBuyAllowed) return false
return when (currency) {
is Currency.Blockchain -> {
if (currency.blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC) {
false
} else {
availableToBuy.contains(currency.currencySymbol)
}
}
is Currency.Token -> false
}
}
fun MoonpayStatus.sellIsAllowed(currency: Currency): Boolean {
if (store.state.globalState.configManager?.config?.isTopUpEnabled == false) return false
if (!isSellAllowed) return false
return when (currency) {
is Currency.Blockchain -> {
if (currency.blockchain == Blockchain.Unknown || currency.blockchain == Blockchain.BSC) {
false
} else {
availableToSell.contains(currency.currencySymbol)
}
}
is Currency.Token -> false
}
}

View file

@ -11,6 +11,8 @@ import com.tangem.common.card.WalletData
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemError
import com.tangem.common.core.TangemSdkError
import com.tangem.common.extensions.guard
import com.tangem.common.extensions.toHexString
import com.tangem.operations.CommandResponse
import com.tangem.operations.PreflightReadMode
@ -63,26 +65,32 @@ class ScanProductTask(val card: Card? = null) : CardSessionRunnable<ScanResponse
session: CardSession,
callback: (result: CompletionResult<ScanResponse>) -> Unit,
) {
ScanTask().run(session) { result ->
when (result) {
is CompletionResult.Success -> {
val card = this.card ?: result.data
val card = this.card ?: session.environment.card.guard {
callback(CompletionResult.Failure(TangemSdkError.MissingPreflightRead()))
return
}
val error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
return@run
}
val error = getErrorIfExcludedCard(card)
if (error != null) {
callback(CompletionResult.Failure(error))
return
}
val commandProcessor = when {
TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
TapWorkarounds.isTangemWallet(card) -> ScanWalletProcessor()
else -> ScanOtherCardsProcessor()
val commandProcessor = when {
TapWorkarounds.isTangemNote(card) -> ScanNoteProcessor()
card.isTangemTwins() -> ScanTwinProcessor()
TapWorkarounds.isTangemWallet(card) -> ScanWalletProcessor()
else -> ScanOtherCardsProcessor()
}
commandProcessor.proceed(card, session) { processorResult ->
when (processorResult) {
is CompletionResult.Success -> ScanTask().run(session) { scanTaskResult ->
when (scanTaskResult) {
is CompletionResult.Success -> callback(CompletionResult.Success(processorResult.data))
is CompletionResult.Failure -> callback(CompletionResult.Failure(scanTaskResult.error))
}
commandProcessor.proceed(card, session, callback)
}
is CompletionResult.Failure -> callback(CompletionResult.Failure(result.error))
is CompletionResult.Failure -> callback(CompletionResult.Failure(processorResult.error))
}
}
}

View file

@ -14,14 +14,14 @@ import com.tangem.tap.network.createMoshi
class CurrenciesRepository(val context: Application) {
private val moshi = createMoshi()
private val blockchainsAdapter: JsonAdapter<Set<Blockchain>> = moshi.adapter(
Types.newParameterizedType(Set::class.java, Blockchain::class.java)
private val blockchainsAdapter: JsonAdapter<List<Blockchain>> = moshi.adapter(
Types.newParameterizedType(List::class.java, Blockchain::class.java)
)
private val tokensAdapter: JsonAdapter<Set<TokenDao>> = moshi.adapter(
Types.newParameterizedType(Set::class.java, TokenDao::class.java)
private val tokensAdapter: JsonAdapter<List<TokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, TokenDao::class.java)
)
private val obsoleteTokensAdapter: JsonAdapter<Set<ObsoleteTokenDao>> = moshi.adapter(
Types.newParameterizedType(Set::class.java, ObsoleteTokenDao::class.java)
private val obsoleteTokensAdapter: JsonAdapter<List<ObsoleteTokenDao>> = moshi.adapter(
Types.newParameterizedType(List::class.java, ObsoleteTokenDao::class.java)
)
fun loadCardCurrencies(cardId: String): CardCurrencies? {
@ -42,58 +42,58 @@ class CurrenciesRepository(val context: Application) {
}
fun saveAddedTokens(cardId: String, tokens: Collection<Token>) {
saveTokens(cardId, loadSavedTokens(cardId) + tokens)
saveTokens(cardId, loadSavedTokens(cardId) + tokens.distinct())
}
fun saveAddedBlockchain(cardId: String, blockchain: Blockchain) {
val blockchains = loadSavedBlockchains(cardId) + blockchain
saveBlockchains(cardId, blockchains)
saveBlockchains(cardId, blockchains.distinct())
}
fun removeToken(cardId: String, token: Token) {
val tokens = loadSavedTokens(cardId).filterNot { it == token }.toSet()
val tokens = loadSavedTokens(cardId).filterNot { it == token }
saveTokens(cardId, tokens)
}
fun removeBlockchain(cardId: String, blockchain: Blockchain) {
val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain }.toSet()
val blockchains = loadSavedBlockchains(cardId).filterNot { it == blockchain }
saveBlockchains(cardId, blockchains)
}
private fun loadSavedTokens(cardId: String): Set<Token> {
private fun loadSavedTokens(cardId: String): List<Token> {
val json = try {
context.readFileText(getFileNameForTokens(cardId))
} catch (exception: Exception) {
return emptySet()
return emptyList()
}
return try {
tokensAdapter.fromJson(json)!!.map { it.toToken() }.toSet()
tokensAdapter.fromJson(json)!!.map { it.toToken() }
} catch (exception: Exception) {
try {
obsoleteTokensAdapter.fromJson(json)!!.map { it.toToken() }.toSet()
obsoleteTokensAdapter.fromJson(json)!!.map { it.toToken() }
} catch (exception: Exception) {
emptySet()
emptyList()
}
}
}
private fun saveTokens(cardId: String, tokens: Set<Token>) {
val json = tokensAdapter.toJson(tokens.map { TokenDao.fromToken(it) }.toSet())
private fun saveTokens(cardId: String, tokens: List<Token>) {
val json = tokensAdapter.toJson(tokens.distinct().map { TokenDao.fromToken(it) })
context.rewriteFile(json, getFileNameForTokens(cardId))
}
private fun loadSavedBlockchains(cardId: String): Set<Blockchain> {
private fun loadSavedBlockchains(cardId: String): List<Blockchain> {
return try {
val json = context.readFileText(getFileNameForBlockchains(cardId))
blockchainsAdapter.fromJson(json) ?: emptySet()
blockchainsAdapter.fromJson(json) ?: emptyList()
} catch (exception: Exception) {
emptySet()
emptyList()
}
}
private fun saveBlockchains(cardId: String, blockchains: Set<Blockchain>) {
val json = blockchainsAdapter.toJson(blockchains)
private fun saveBlockchains(cardId: String, blockchains: List<Blockchain>) {
val json = blockchainsAdapter.toJson(blockchains.distinct())
context.rewriteFile(json, getFileNameForBlockchains(cardId))
}
@ -120,7 +120,10 @@ class CurrenciesRepository(val context: Application) {
return tokensAdapter.fromJson(ethereumTokensJson)!!.map { it.toToken() } +
tokensAdapter.fromJson(bscTokensJson)!!.map { it.toToken() } +
tokensAdapter.fromJson(binanceTokensJson)!!.map { it.toToken() }
tokensAdapter.fromJson(binanceTokensJson)!!.mapNotNull {
// temporary exclude Binance BEP-8 tokens
if (it.type != null && it.type == BINANCE_TOKEN_TYPE_BEP8) null else it.toToken()
}
}
fun getBlockchains(
@ -144,6 +147,8 @@ class CurrenciesRepository(val context: Application) {
private const val FILE_NAME_PREFIX_TOKENS = "tokens"
private const val FILE_NAME_PREFIX_BLOCKCHAINS = "blockchains"
private const val BINANCE_TOKEN_TYPE_BEP8 = "bep8"
fun getFileNameForTokens(cardId: String): String = "${FILE_NAME_PREFIX_TOKENS}_$cardId"
fun getFileNameForBlockchains(cardId: String): String =
"${FILE_NAME_PREFIX_BLOCKCHAINS}_$cardId"
@ -157,7 +162,8 @@ data class TokenDao(
val contractAddress: String,
val decimalCount: Int,
@Json(name = "blockchain")
val blockchainDao: BlockchainDao
val blockchainDao: BlockchainDao,
val type: String? = null
) {
fun toToken(): Token {
return Token(
@ -224,12 +230,12 @@ data class ObsoleteTokenDao(
@JsonClass(generateAdapter = true)
data class CardCurrenciesDao(
val tokens: Set<TokenDao>,
val blockchains: Set<Blockchain>,
val tokens: List<TokenDao>,
val blockchains: List<Blockchain>,
) {
fun toCardCurrencies(): CardCurrencies {
return CardCurrencies(
tokens = tokens.map { it.toToken() }.toSet(),
tokens = tokens.map { it.toToken() }.distinct(),
blockchains = blockchains
)
}
@ -237,7 +243,7 @@ data class CardCurrenciesDao(
companion object {
fun fromCardCurrencies(cardCurrencies: CardCurrencies): CardCurrenciesDao {
return CardCurrenciesDao(
tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }.toSet(),
tokens = cardCurrencies.tokens.map { TokenDao.fromToken(it) }.distinct(),
blockchains = cardCurrencies.blockchains
)
}
@ -245,6 +251,6 @@ data class CardCurrenciesDao(
}
data class CardCurrencies(
val tokens: Set<Token>,
val blockchains: Set<Blockchain>,
val tokens: List<Token>,
val blockchains: List<Blockchain>,
)

View file

@ -38,15 +38,6 @@ class TradeCryptoHelper {
private const val TRANSACTION_RECEIPT_PATH = "transaction_receipt?transactionId="
val AVAILABLE_TO_BUY: Set<String> = setOf(
"ZRX", "AAVE", "ALGO", "AXS", "BAT", "BNB", "BUSD", "BTC", "BCH", "BTT", "ADA", "CELO", "CUSD", "LINK", "CHZ", "COMP", "ATOM", "DAI", "DASH", "MANA", "DGB", "DOGE", "EGLD",
"ENJ", "EOS", "ETC", "ETH", "KETH", "RINKETH", "FIL", "HBAR", "MIOTA", "KAVA", "KLAY", "LBC", "LTC", "LUNA", "MKR", "OM", "MATIC", "NANO", "NEAR", "XEM", "NEO", "NIM", "OKB",
"OMG", "ONG", "ONT", "DOT", "QTUM", "RVN", "RFUEL", "KEY", "SRM", "SOL", "XLM", "STMX", "SNX", "KRT", "UST", "USDT", "XTZ", "RUNE", "SAND", "TOMO", "AVA", "TRX", "TUSD", "UNI",
"USDC", "UTK", "VET", "WAXP", "WBTC", "XRP", "ZEC", "ZIL"
)
val AVAILABLE_TO_SELL: Set<String> = setOf("BTC", "ETH", "BCH")
fun getUrl(
action: Action,
blockchain: Blockchain?,

View file

@ -6,12 +6,13 @@ import com.tangem.common.KeyPair
import com.tangem.common.card.EllipticCurve
import com.tangem.common.core.CardSession
import com.tangem.common.core.CardSessionRunnable
import com.tangem.common.core.TangemSdkError
import com.tangem.common.core.TangemError
import com.tangem.common.extensions.hexToBytes
import com.tangem.operations.wallet.CreateWalletCommand
import com.tangem.operations.wallet.CreateWalletResponse
import com.tangem.operations.wallet.PurgeWalletCommand
import com.tangem.tap.domain.extensions.getSingleWallet
import com.tangem.wallet.R
class CreateSecondTwinWalletTask(
private val firstPublicKey: String,
@ -25,11 +26,11 @@ class CreateSecondTwinWalletTask(
val publicKey = card?.getSingleWallet()?.publicKey
if (publicKey != null) {
if (!card.cardId.startsWith(TwinsHelper.getPairCardSeries(firstCardId) ?: "")) {
callback(CompletionResult.Failure(TangemSdkError.WrongCardType()))
callback(CompletionResult.Failure(WrongTwinCard()))
return
}
session.setInitialMessage(preparingMessage)
session.setMessage(preparingMessage)
PurgeWalletCommand(publicKey).run(session) { response ->
when (response) {
is CompletionResult.Success -> {
@ -45,7 +46,7 @@ class CreateSecondTwinWalletTask(
}
private fun finishTask(session: CardSession, callback: (result: CompletionResult<CreateWalletResponse>) -> Unit) {
session.setInitialMessage(creatingWalletMessage)
session.setMessage(creatingWalletMessage)
CreateWalletCommand(EllipticCurve.Secp256k1).run(session) { result ->
when (result) {
is CompletionResult.Success -> {
@ -65,4 +66,10 @@ class CreateSecondTwinWalletTask(
}
}
}
private class WrongTwinCard : TangemError {
override val code: Int = 50005
override var customMessage: String = code.toString()
override val messageResId = R.string.twins_wrong_card_error
}
}

View file

@ -1,6 +1,7 @@
package com.tangem.tap.features.details.redux
import com.tangem.blockchain.common.Wallet
import com.tangem.common.card.Card
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.common.redux.global.FiatCurrencyName
@ -50,7 +51,7 @@ sealed class DetailsAction : Action {
}
sealed class ManageSecurity : DetailsAction() {
data class CheckCurrentSecurityOption(val cardId: String?) : ManageSecurity()
data class CheckCurrentSecurityOption(val card: Card) : ManageSecurity()
data class SetCurrentOption(val userCodes: CheckUserCodesResponse) : ManageSecurity()
object OpenSecurity : ManageSecurity()
data class SelectOption(val option: SecurityOption) : ManageSecurity()

View file

@ -1,8 +1,10 @@
package com.tangem.tap.features.details.redux
import com.tangem.common.CompletionResult
import com.tangem.common.card.FirmwareVersion
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.operations.pins.CheckUserCodesResponse
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchNotification
import com.tangem.tap.common.extensions.dispatchOnMain
@ -100,7 +102,7 @@ class DetailsMiddleware {
store.dispatch(NavigationAction.PopBackTo())
}
is DetailsAction.EraseWallet.Confirm -> {
val card = store.state.detailsState.card ?: return
val card = store.state.detailsState.scanResponse?.card ?: return
scope.launch {
val result = tangemSdkManager.eraseWallet(card)
withContext(Dispatchers.Main) {
@ -113,7 +115,7 @@ class DetailsMiddleware {
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.PurgeWallet,
card = store.state.detailsState.card
card = store.state.detailsState.scanResponse?.card
)
}
}
@ -141,16 +143,22 @@ class DetailsMiddleware {
fun handle(action: DetailsAction.ManageSecurity) {
when (action) {
is DetailsAction.ManageSecurity.CheckCurrentSecurityOption -> {
scope.launch {
when (val response = tangemSdkManager.checkUserCodes(action.cardId)) {
is CompletionResult.Success -> {
store.dispatchOnMain(
DetailsAction.ManageSecurity.SetCurrentOption(response.data)
)
store.dispatchOnMain(DetailsAction.ManageSecurity.OpenSecurity)
}
is CompletionResult.Failure -> {
if (action.card.firmwareVersion >= FirmwareVersion.IsAccessCodeStatusAvailable) {
// for a card that meets this condition, we can get these statuses from it
val simulatedResponse = CheckUserCodesResponse(
action.card.isAccessCodeSet, action.card.isPasscodeSet ?: false
)
store.dispatch(DetailsAction.ManageSecurity.SetCurrentOption(simulatedResponse))
store.dispatch(DetailsAction.ManageSecurity.OpenSecurity)
} else {
scope.launch {
when (val response = tangemSdkManager.checkUserCodes(action.card.cardId)) {
is CompletionResult.Success -> {
store.dispatchOnMain(DetailsAction.ManageSecurity.SetCurrentOption(response.data))
store.dispatchOnMain(DetailsAction.ManageSecurity.OpenSecurity)
}
is CompletionResult.Failure -> {
}
}
}
}
@ -170,7 +178,7 @@ class DetailsMiddleware {
}
}
is DetailsAction.ManageSecurity.SaveChanges -> {
val cardId = store.state.detailsState.card?.cardId
val cardId = store.state.detailsState.scanResponse?.card?.cardId
val selectedOption = store.state.detailsState.securityScreenState?.selectedOption
scope.launch {
val result = when (selectedOption) {
@ -197,9 +205,9 @@ class DetailsMiddleware {
actionToLog = FirebaseAnalyticsHandler.ActionToLog.ChangeSecOptions,
parameters = mapOf(
FirebaseAnalyticsHandler.AnalyticsParam.NEW_SECURITY_OPTION to
(selectedOption?.name ?: "")
(selectedOption?.name ?: "")
),
card = store.state.detailsState.card
card = store.state.detailsState.scanResponse?.card
)
}
store.dispatch(DetailsAction.ManageSecurity.SaveChanges.Failure)

View file

@ -41,7 +41,7 @@ private fun internalReduce(action: Action, state: AppState): DetailsState {
private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: DetailsState): DetailsState {
return DetailsState(
card = action.scanResponse.card,
scanResponse = action.scanResponse,
wallets = action.wallets,
cardInfo = action.scanResponse.card.toCardInfo(),
appCurrencyState = AppCurrencyState(action.fiatCurrencyName),
@ -52,8 +52,10 @@ private fun handlePrepareScreen(action: DetailsAction.PrepareScreen, state: Deta
private fun handleEraseWallet(action: DetailsAction.EraseWallet, state: DetailsState): DetailsState {
return when (action) {
DetailsAction.EraseWallet.Check -> {
val notAllowedByAnyWallet = state.card?.wallets?.any { it.settings.isPermanent } ?: false
val notAllowedByCard = notAllowedByAnyWallet || state.card?.isWalletDataSupported == true
val card = state.scanResponse?.card
val notAllowedByAnyWallet = card?.wallets?.any { it.settings.isPermanent } ?: false
val notAllowedByCard = notAllowedByAnyWallet ||
(card?.isWalletDataSupported == true && !state.scanResponse.isTangemNote())
val notEmpty = state.wallets.any {
it.hasPendingTransactions() || it.amounts.toSendableAmounts().isNotEmpty()
}
@ -121,7 +123,9 @@ private fun handleSecurityAction(
state.copy(securityScreenState = SecurityScreenState(currentOption = securityOption))
}
is DetailsAction.ManageSecurity.OpenSecurity -> {
if (state.card?.isStart2Coin == true) {
if (state.scanResponse?.card?.isStart2Coin == true ||
state.scanResponse?.isTangemNote() == true
) {
return state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = EnumSet.of(SecurityOption.LongTap),
selectedOption = state.securityScreenState.currentOption
@ -129,7 +133,7 @@ private fun handleSecurityAction(
}
val allowedSecurityOptions = prepareAllowedSecurityOptions(
state.card, state.securityScreenState?.currentOption
state.scanResponse?.card, state.securityScreenState?.currentOption
)
state.copy(securityScreenState = state.securityScreenState?.copy(
allowedOptions = allowedSecurityOptions,
@ -154,7 +158,7 @@ private fun handleSecurityAction(
state.copy(
securityScreenState = state.securityScreenState?.copy(
currentOption = state.securityScreenState.selectedOption,
allowedOptions = state.card?.let {
allowedOptions = state.scanResponse?.card?.let {
prepareAllowedSecurityOptions(
it, state.securityScreenState.selectedOption
)
@ -189,7 +193,7 @@ private fun prepareAllowedSecurityOptions(
}
private fun Card.toCardInfo(): CardInfo? {
private fun Card.toCardInfo(): CardInfo {
val cardId = this.cardId.chunked(4).joinToString(separator = " ")
val issuer = this.issuer.name
val signedHashes = this.signedHashesCount()

View file

@ -2,10 +2,10 @@ package com.tangem.tap.features.details.redux
import android.net.Uri
import com.tangem.blockchain.common.Wallet
import com.tangem.common.card.Card
import com.tangem.tap.common.entities.Button
import com.tangem.tap.common.entities.TapCurrency.Companion.DEFAULT_FIAT_CURRENCY
import com.tangem.tap.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.tasks.product.ScanResponse
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.network.coinmarketcap.FiatCurrency
import com.tangem.tap.store
@ -14,7 +14,7 @@ import java.util.*
import kotlin.properties.ReadOnlyProperty
data class DetailsState(
val card: Card? = null,
val scanResponse: ScanResponse? = null,
val wallets: List<Wallet> = emptyList(),
val cardInfo: CardInfo? = null,
val appCurrencyState: AppCurrencyState = AppCurrencyState(),

View file

@ -10,6 +10,7 @@ import com.tangem.tap.common.extensions.show
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.domain.isMultiwalletAllowed
import com.tangem.tap.domain.twins.getTwinCardIdForUser
import com.tangem.tap.features.details.redux.DetailsAction
import com.tangem.tap.features.details.redux.DetailsState
@ -66,7 +67,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
if (state.cardInfo != null) {
val cardId = if (state.isTangemTwins) {
state.card?.getTwinCardIdForUser()
state.scanResponse?.card?.getTwinCardIdForUser()
} else {
state.cardInfo.cardId
}
@ -112,12 +113,13 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
store.dispatch(GlobalAction.SendFeedback(FeedbackEmail()))
}
tv_wallet_connect.show(state.scanResponse?.card?.isMultiwalletAllowed == true)
tv_wallet_connect.setOnClickListener {
store.dispatch(NavigationAction.NavigateTo(AppScreen.WalletConnectSessions))
}
tv_security_title.setOnClickListener {
store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.card?.cardId))
store.dispatch(DetailsAction.ManageSecurity.CheckCurrentSecurityOption(state.scanResponse!!.card))
}
val currentSecurity = when (state.securityScreenState?.currentOption) {
@ -129,7 +131,7 @@ class DetailsFragment : Fragment(R.layout.fragment_details), StoreSubscriber<Det
currentSecurity?.let { tv_security.text = getString(it) }
if (state.appCurrencyState.showAppCurrencyDialog &&
!state.appCurrencyState.fiatCurrencies.isNullOrEmpty()) {
!state.appCurrencyState.fiatCurrencies.isNullOrEmpty()) {
currencySelectionDialog.show(
state.appCurrencyState.fiatCurrencies,
state.appCurrencyState.fiatCurrencyName,

View file

@ -25,8 +25,8 @@ class DisclaimerFragment : Fragment(R.layout.fragment_disclaimer),
}
})
val inflater = TransitionInflater.from(requireContext())
enterTransition = inflater.inflateTransition(R.transition.slide_right)
exitTransition = inflater.inflateTransition(R.transition.fade)
enterTransition = inflater.inflateTransition(android.R.transition.slide_bottom)
exitTransition = inflater.inflateTransition(android.R.transition.slide_top)
}
override fun onStart() {

View file

@ -1,19 +1,14 @@
package com.tangem.tap.features.feedback
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.core.app.ShareCompat
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.tangem.Log
import com.tangem.TangemSdkLogger
import com.tangem.blockchain.common.*
import com.tangem.common.card.Card
import com.tangem.tap.common.extensions.sendEmail
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.TapWorkarounds
import timber.log.Timber
@ -28,22 +23,26 @@ import java.util.*
[REDACTED_AUTHOR]
*/
class FeedbackManager(
val infoHolder: AdditionalEmailInfo,
private val logCollector: TangemLogCollector,
val infoHolder: AdditionalEmailInfo,
private val logCollector: TangemLogCollector,
) {
private lateinit var activity: Activity
fun updateAcivity(activity: Activity) {
fun updateActivity(activity: Activity) {
this.activity = activity
}
fun send(emailData: EmailData) {
fun send(emailData: EmailData, onFail: ((Exception) -> Unit)? = null) {
if (!this::activity.isInitialized) return
emailData.prepare(infoHolder)
val fileLog = if (emailData is ScanFailsEmail) createLogFile() else null
sendTo(
activity.sendEmail(
email = getSupportEmail(),
subject = emailData.subject, message = emailData.joinTogether(infoHolder),
fileLog = fileLog
file = fileLog,
onFail = onFail
)
}
@ -55,42 +54,6 @@ class FeedbackManager(
}
}
private fun sendTo(email: String, subject: String, message: String, fileLog: File? = null) {
val emailFilterIntent = Intent(Intent.ACTION_SENDTO, Uri.parse("mailto:"))
val originalIntentResults = activity.packageManager.queryIntentActivities(emailFilterIntent, 0)
val emailFilterIntentResults = activity.packageManager.queryIntentActivities(emailFilterIntent, 0)
val targetedIntents = originalIntentResults
.filter { originalResult ->
emailFilterIntentResults.any {
originalResult.activityInfo.packageName == it.activityInfo.packageName
}
}
.map {
createEmailShareIntent(email, subject, message, fileLog).apply {
setPackage(it.activityInfo.packageName)
}
}
.toMutableList()
try {
val chooserIntent = Intent.createChooser(targetedIntents.removeAt(0), "Send mail...")
chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, targetedIntents.toTypedArray())
chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
ContextCompat.startActivity(activity, chooserIntent, null)
} catch (ex: ActivityNotFoundException) {
Timber.e(ex)
}
}
private fun createEmailShareIntent(recipient: String, subject: String, text: String, file: File? = null): Intent {
val builder = ShareCompat.IntentBuilder.from(activity)
.setType("message/rfc822")
.setEmailTo(arrayOf(recipient))
.setSubject(subject)
.setText(text)
file?.let { builder.setStream(FileProvider.getUriForFile(activity, "${activity.packageName}.provider", it)) }
return builder.intent
}
private fun createLogFile(): File? {
return try {
val file = File(activity.filesDir, "logs.txt")
@ -134,12 +97,12 @@ class TangemLogCollector : TangemSdkLogger {
class AdditionalEmailInfo {
class EmailWalletInfo(
var blockchain: Blockchain = Blockchain.Unknown,
var address: String = "",
var explorerLink: String = "",
var host: String = "",
// var outputsCount: String = ""
// var transactionHex: String = ""
var blockchain: Blockchain = Blockchain.Unknown,
var address: String = "",
var explorerLink: String = "",
var host: String = "",
// var outputsCount: String = ""
// var transactionHex: String = ""
)
var appVersion: String = ""
@ -151,6 +114,7 @@ class AdditionalEmailInfo {
// wallets
internal val walletsInfo = mutableListOf<EmailWalletInfo>()
internal val tokens = mutableMapOf<Blockchain, Collection<Token>>()
internal var onSendErrorWalletInfo: EmailWalletInfo? = null
var signedHashesCount: String = ""
@ -178,11 +142,12 @@ class AdditionalEmailInfo {
cardFirmwareVersion = card.firmwareVersion.stringValue
cardIssuer = card.issuer.name
signedHashesCount = card.wallets
.joinToString(";") { "${it.curve?.curve} - ${it.totalSignedHashes}" }
.joinToString(";") { "${it.curve?.curve} - ${it.totalSignedHashes}" }
}
fun setWalletsInfo(walletManagers: List<WalletManager>) {
walletsInfo.clear()
tokens.clear()
walletManagers.forEach { manager ->
walletsInfo.add(
EmailWalletInfo(
@ -192,6 +157,9 @@ class AdditionalEmailInfo {
host = manager.currentHost
)
)
if (manager.cardTokens.isNotEmpty()) {
tokens[manager.wallet.blockchain] = manager.cardTokens
}
}
}
@ -235,10 +203,21 @@ class AdditionalEmailInfo {
interface EmailData {
val subject: String
val mainMessage: String
fun prepare(infoHolder: AdditionalEmailInfo) {}
fun appendDelimiter(builder: StringBuilder) {
builder.append("----------\n")
}
fun appendBlankLine(builder: StringBuilder) {
builder.append("\n")
}
fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String
fun joinTogether(infoHolder: AdditionalEmailInfo): String {
return "$mainMessage\n\n\n\n\n" +
return "$mainMessage\n\n\n\n" +
"Following information is optional. You can erase it if you dont want to share it.\n" +
createOptionalMessage(infoHolder)
}
@ -253,6 +232,7 @@ class RateCanBeBetterEmail : EmailData {
return StringBuilder().apply {
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
appendBlankLine(this)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
@ -265,6 +245,7 @@ class ScanFailsEmail : EmailData {
override val mainMessage: String = "Please tell us what card do you have?"
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
return StringBuilder().apply {
appendBlankLine(this)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
@ -275,31 +256,50 @@ class ScanFailsEmail : EmailData {
class SendTransactionFailedEmail(private val error: String) : EmailData {
override val subject: String = "Cant send a transaction"
override val mainMessage: String = "Please tell us more about your issue. Every small detail can help."
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
val walletInfo = infoHolder.onSendErrorWalletInfo ?: AdditionalEmailInfo.EmailWalletInfo()
return StringBuilder().apply {
appendKeyValue("Error", error)
appendKeyValue("Card ID", infoHolder.cardId)
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
appendDelimiter(this)
appendKeyValue("Blockchain", walletInfo.blockchain.fullName)
appendKeyValue("Host", walletInfo.host)
appendKeyValue("Token", infoHolder.token)
appendKeyValue("Error", error)
appendDelimiter(this)
appendKeyValue("Source address", walletInfo.address)
appendKeyValue("Destination address", infoHolder.destinationAddress)
appendKeyValue("Amount", infoHolder.amount)
appendKeyValue("Fee", infoHolder.fee)
appendBlankLine(this)
appendKeyValue("Phone model", infoHolder.phoneModel)
appendKeyValue("OS version", infoHolder.osVersion)
appendKeyValue("App version", infoHolder.appVersion)
appendKeyValue("Firmware version", infoHolder.cardFirmwareVersion)
appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
// appendKeyValue("Transaction HEX", infoHolder.transactionHex)
}.toString()
}
}
class FeedbackEmail : EmailData {
override val subject: String = "Tangem Tap feedback"
override val mainMessage: String = "Hi Tangem,"
override val subject: String
get() = if (isS2CCard) s2cSubject else tangemSubject
override val mainMessage: String
get() = if (isS2CCard) s2cMainMessage else tangemMainMessage
private val tangemSubject = "Tangem feedback"
private val tangemMainMessage = "Hi support team,"
private val s2cSubject = "Feedback"
private val s2cMainMessage = "Hi support team,"
private var isS2CCard = false
override fun prepare(infoHolder: AdditionalEmailInfo) {
isS2CCard = TapWorkarounds.isStart2CoinIssuer(infoHolder.cardIssuer)
}
override fun createOptionalMessage(infoHolder: AdditionalEmailInfo): String {
val builder = StringBuilder()
builder.appendKeyValue("Card ID", infoHolder.cardId)
@ -307,11 +307,22 @@ class FeedbackEmail : EmailData {
builder.appendKeyValue("Signed hashes", infoHolder.signedHashesCount)
infoHolder.walletsInfo.forEach {
appendDelimiter(builder)
builder.appendKeyValue("Blockchain", it.blockchain.fullName)
builder.appendKeyValue("Host", it.host)
builder.appendKeyValue("Wallet address", it.address)
builder.appendKeyValue("Explorer link", it.explorerLink)
}
appendBlankLine(builder)
infoHolder.tokens.forEach { tokens ->
appendDelimiter(builder)
builder.appendKeyValue("Blockchain", tokens.key.fullName)
builder.appendKeyValue("Tokens", tokens.value.map { "${it.name} - ${it.symbol}" }.toString())
}
appendDelimiter(builder)
appendBlankLine(builder)
// appendKeyValue("Outputs count", infoHolder.outputsCount)
builder.appendKeyValue("Phone model", infoHolder.phoneModel)
builder.appendKeyValue("OS version", infoHolder.osVersion)

View file

@ -18,6 +18,7 @@ import com.tangem.wallet.R
import kotlinx.android.synthetic.main.fragment_onboarding_main.*
import kotlinx.android.synthetic.main.layout_onboarding_container_bottom.*
import kotlinx.android.synthetic.main.layout_onboarding_home_top.*
import kotlinx.android.synthetic.main.view_bg_home.*
class HomeFragment : BaseOnboardingFragment<HomeState>() {
@ -39,7 +40,10 @@ class HomeFragment : BaseOnboardingFragment<HomeState>() {
val shareTransition = FragmentShareTransition(
listOf(
ShareElement(imv_front_card, ShareElement.imvFrontCard),
ShareElement(imv_back_card, ShareElement.imvBackCard)
ShareElement(imv_back_card, ShareElement.imvBackCard),
ShareElement(bg_circle_large),
ShareElement(bg_circle_medium),
ShareElement(bg_circle_min),
),
FrontCardEnterTransition(),
FrontCardExitTransition()

View file

@ -37,7 +37,7 @@ private val homeMiddleware: Middleware<AppState> = { dispatch, state ->
when (action) {
is HomeAction.Init -> {
store.dispatch(GlobalAction.RestoreAppCurrency)
store.dispatch(GlobalAction.GetMoonPayUserStatus)
store.dispatch(GlobalAction.GetMoonPayStatus)
store.dispatch(HomeAction.SetTermsOfUseState(preferencesStorage.wasDisclaimerAccepted()))
}
is HomeAction.ShouldScanCardOnResume -> {
@ -60,6 +60,7 @@ private fun handleReadCard() {
} else {
changeButtonState(ButtonState.PROGRESS)
store.dispatch(GlobalAction.ScanCard({ scanResponse ->
store.state.globalState.tapWalletManager.updateConfigManager(scanResponse)
store.dispatch(TwinCardsAction.IfTwinsPrepareState(scanResponse))
if (OnboardingHelper.isOnboardingCase(scanResponse)) {

View file

@ -4,7 +4,10 @@ import android.content.Context
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.tangem.tap.common.extensions.copyToClipboard
import com.tangem.tap.common.extensions.dispatchDialogHide
import com.tangem.tap.common.extensions.dispatchShare
import com.tangem.tap.common.extensions.dispatchToastNotification
import com.tangem.tap.common.redux.AppDialog
import com.tangem.tap.features.wallet.redux.Currency
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.dialog_onboarding_address_info.*
@ -35,10 +38,31 @@ class AddressInfoBottomSheetDialog(
tv_address.text = data.address
btn_fl_copy_address.setOnClickListener {
context.copyToClipboard(data.address)
stateDialog.onCopyAddress()
store.dispatchToastNotification(R.string.copy_toast_msg)
}
btn_fl_explore.setOnClickListener {
stateDialog.onExploreAddress()
btn_fl_share.setOnClickListener {
store.dispatchShare(data.shareUrl)
}
tv_recieve_message.text = getQRReceiveMessage(tv_recieve_message.context, stateDialog.currency)
}
}
fun getQRReceiveMessage(context: Context, currency: Currency): String {
return when (currency) {
is Currency.Blockchain -> {
context.getString(
R.string.address_qr_code_message_format,
currency.blockchain.fullName,
currency.currencySymbol
)
}
is Currency.Token -> {
context.getString(
R.string.address_qr_code_message_token_format,
currency.token.name,
currency.currencySymbol,
currency.blockchain.fullName
)
}
}
}

View file

@ -99,10 +99,11 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
val currency = state.walletBalance.currency.currencySymbol
tv_balance_value.text = "$balanceValue $currency"
tv_balance_value.text = balanceValue
tv_balance_currency.text = state.walletBalance.currency.currencySymbol
} else {
tv_balance_value.text = ""
tv_balance_currency.text = ""
}
}
@ -132,17 +133,29 @@ class OnboardingNoteFragment : BaseOnboardingFragment<OnboardingNoteState>() {
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_circle))
updateConstraints(state.currentStep, R.layout.lp_onboarding_create_wallet)
btn_alternative_action.isVisible = false // temporary
}
private fun setupTopUpWalletState(state: OnboardingNoteState) {
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener {
store.dispatch(OnboardingNoteAction.TopUp)
}
if (state.isBuyAllowed) {
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener {
store.dispatch(OnboardingNoteAction.TopUp)
}
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
btn_alternative_action.isVisible = true
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
} else {
btn_main_action.setText(R.string.onboarding_button_receive_crypto)
btn_main_action.setOnClickListener {
store.dispatch(OnboardingNoteAction.ShowAddressInfoDialog)
}
btn_alternative_action.isVisible = false
}
tv_header.setText(R.string.onboarding_top_up_header)

View file

@ -134,12 +134,8 @@ private fun handleNoteAction(action: Action, dispatch: DispatchFunction) {
}
is OnboardingNoteAction.ShowAddressInfoDialog -> {
val addressData = noteState.walletManager?.getAddressData() ?: return
val addressWasCopied = globalState.resources.strings.addressWasCopied
val appDialog = AppDialog.AddressInfoDialog(
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
onExploreAddress = { store.dispatchOpenUrl(addressData.exploreUrl) }
)
val appDialog = AppDialog.AddressInfoDialog(noteState.walletBalance.currency, addressData)
store.dispatchDialogShow(appDialog)
}
is OnboardingNoteAction.TopUp -> {

View file

@ -2,8 +2,11 @@ package com.tangem.tap.features.onboarding.products.note.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.buyIsAllowed
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.store
import org.rekotlin.StateType
import kotlin.properties.ReadOnlyProperty
/**
[REDACTED_AUTHOR]
@ -22,6 +25,10 @@ data class OnboardingNoteState(
val progress: Int
get() = steps.indexOf(currentStep)
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
store.state.globalState.moonpayStatus?.buyIsAllowed(walletBalance.currency) ?: false
}
}
enum class OnboardingNoteStep {

View file

@ -87,6 +87,8 @@ class OnboardingOtherCardsFragment : BaseOnboardingFragment<OnboardingOtherCards
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_circle))
updateConstraints(R.layout.lp_onboarding_create_wallet)
btn_alternative_action.isVisible = false // temporary
}
private fun setupDoneState(state: OnboardingOtherCardsState) {

View file

@ -178,7 +178,16 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
is Result.Success -> {
updateScanResponse(result.data)
delay(DELAY_SDK_DIALOG_CLOSE)
withMainContext { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet)) }
withMainContext {
when (twinCardsState.mode) {
CreateTwinWalletMode.CreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.TopUpWallet))
}
CreateTwinWalletMode.RecreateWallet -> {
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.Done))
}
}
}
}
is Result.Failure -> {
}
@ -230,12 +239,7 @@ private fun handle(action: Action, dispatch: DispatchFunction) {
is TwinCardsAction.ShowAddressInfoDialog -> {
val addressData = twinCardsState.walletManager?.getAddressData() ?: return
val addressWasCopied = globalState.resources.strings.addressWasCopied
val appDialog = AppDialog.AddressInfoDialog(
addressData,
onCopyAddress = { store.dispatchToastNotification(addressWasCopied) },
onExploreAddress = { store.dispatchOpenUrl(addressData.exploreUrl) }
)
val appDialog = AppDialog.AddressInfoDialog(twinCardsState.walletBalance.currency, addressData)
store.dispatchDialogShow(appDialog)
}
is TwinCardsAction.TopUp -> {

View file

@ -2,10 +2,13 @@ package com.tangem.tap.features.onboarding.products.twins.redux
import com.tangem.blockchain.common.WalletManager
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.extensions.buyIsAllowed
import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.domain.twins.TwinCardsManager
import com.tangem.tap.features.onboarding.OnboardingWalletBalance
import com.tangem.tap.store
import org.rekotlin.StateType
import kotlin.properties.ReadOnlyProperty
/**
[REDACTED_AUTHOR]
@ -52,6 +55,10 @@ data class TwinCardsState(
val showAlert: Boolean
get() = currentStep == TwinCardsStep.CreateSecondWallet || currentStep == TwinCardsStep.CreateThirdWallet
val isBuyAllowed: Boolean by ReadOnlyProperty<Any, Boolean> { thisRef, property ->
store.state.globalState.moonpayStatus?.buyIsAllowed(walletBalance.currency) ?: false
}
}
enum class CreateTwinWalletMode { CreateWallet, RecreateWallet }

View file

@ -14,7 +14,6 @@ import com.tangem.blockchain.common.Blockchain
import com.tangem.common.extensions.VoidCallback
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.leapfrogWidget.LeapfrogWidget
import com.tangem.tap.common.postUi
import com.tangem.tap.common.redux.navigation.AppScreen
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.common.redux.navigation.ShareElement
@ -38,7 +37,6 @@ import kotlinx.android.synthetic.main.layout_onboarding_container_top.*
import kotlinx.android.synthetic.main.view_bg_twins_welcome.*
import kotlinx.android.synthetic.main.view_onboarding_progress.*
import kotlinx.android.synthetic.main.view_onboarding_tv_balance.*
import org.rekotlin.Action
class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
@ -102,6 +100,9 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
imv_twin_front_card.transitionName = ShareElement.imvFrontCard
imv_twin_back_card.transitionName = ShareElement.imvBackCard
// if don't this, the bg_circle_... is overflow the app_bar
app_bar.bringToFront()
}
override fun subscribeToStore() {
@ -140,35 +141,42 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
if (state.balanceCriticalError == null) {
val balanceValue = state.walletBalance.value.stripZeroPlainString()
val currency = state.walletBalance.currency.currencySymbol
tv_balance_value.text = "$balanceValue $currency"
tv_balance_value.text = balanceValue
tv_balance_currency.text = state.walletBalance.currency.currencySymbol
} else {
tv_balance_value.text = ""
tv_balance_currency.text = ""
}
}
private fun setupWelcomeOnlyState(state: TwinCardsState) {
setupWelcomeState(state, NavigationAction.NavigateTo(AppScreen.Wallet))
setupWelcomeState(state) {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Wallet))
store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.None))
}
}
private fun setupWelcomeState(state: TwinCardsState) {
setupWelcomeState(state, TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet))
setupWelcomeState(state) { store.dispatch(TwinCardsAction.SetStepOfScreen(TwinCardsStep.CreateFirstWallet)) }
}
private fun setupWelcomeState(state: TwinCardsState, mainAction: Action) {
private fun setupWelcomeState(state: TwinCardsState, mainAction: VoidCallback) {
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
onboarding_twins_welcome_bg.show()
pb_state.hide()
tv_header.setText(R.string.twins_onboarding_subtitle)
tv_body.text = getString(R.string.twins_onboarding_description_format, state.cardNumber?.pairIndexNumber())
btn_main_action.setText(R.string.common_continue)
btn_main_action.setOnClickListener { store.dispatch(mainAction) }
btn_main_action.setOnClickListener { mainAction() }
}
private fun setupWarningState(state: TwinCardsState) {
twinsWidget.toWelcome(false) { startPostponedEnterTransition() }
onboarding_twins_welcome_bg.hide()
pb_state.hide()
chb_understand.show()
@ -186,6 +194,7 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
}
private fun setupCreateFirstWalletState(state: TwinCardsState) {
onboarding_twins_welcome_bg.hide()
bg_circle_large.hide()
bg_circle_medium.hide()
bg_circle_min.hide()
@ -283,15 +292,24 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
}
}
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.TopUp)
}
if (state.isBuyAllowed) {
btn_main_action.setText(R.string.onboarding_top_up_button_but_crypto)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.TopUp)
}
btn_alternative_action.isVisible = true
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
btn_alternative_action.isVisible = true
btn_alternative_action.setText(R.string.onboarding_top_up_button_show_wallet_address)
btn_alternative_action.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
} else {
btn_main_action.setText(R.string.onboarding_button_receive_crypto)
btn_main_action.setOnClickListener {
store.dispatch(TwinCardsAction.ShowAddressInfoDialog)
}
btn_alternative_action.isVisible = false
}
tv_header.setText(R.string.onboarding_top_up_header)
@ -299,13 +317,10 @@ class TwinsCardsFragment : BaseOnboardingFragment<TwinCardsState>() {
btnRefreshBalanceWidget.changeState(state.walletBalance.state)
if (btnRefreshBalanceWidget.isShowing != true) {
postUi(300) {
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(TwinCardsAction.Balance.Update)
}
btnRefreshBalanceWidget.mainView.setOnClickListener {
store.dispatch(TwinCardsAction.Balance.Update)
}
}
imv_card_background.setBackgroundDrawable(requireContext().getDrawableCompat(R.drawable.shape_rectangle_rounded_8))
updateConstraints(state.currentStep, R.layout.lp_onboarding_topup_wallet_twins)
}

View file

@ -15,9 +15,8 @@ class CreateWalletInterruptDialog {
companion object {
fun create(state: TwinCardsAction.Wallet.ShowInterruptDialog, context: Context): AlertDialog {
return MaterialAlertDialogBuilder(context)
.setMessage(R.string.twins_recreate_alert)
.setPositiveButton(R.string.common_ok) { _, _ -> state.onOk() }
.setNegativeButton(R.string.common_cancel) { _, _ -> }
.setMessage(R.string.onboarding_twin_exit_warning)
.setPositiveButton(R.string.warning_button_ok) { _, _ -> }
.setOnDismissListener { store.dispatchDialogHide() }
.create()
}

View file

@ -58,6 +58,10 @@ sealed class TransactionExtrasAction : SendScreenActionUi {
data class HandleUserInput(val data: String) : XlmMemo()
}
sealed class BinanceMemo : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : BinanceMemo()
}
sealed class XrpDestinationTag : TransactionExtrasAction() {
data class HandleUserInput(val data: String) : XrpDestinationTag()
}

View file

@ -68,9 +68,9 @@ class AmountMiddleware {
} else {
val amountErrors = extractErrorsForAmountField(transactionErrors)
if (amountErrors.isNotEmpty()) {
transactionErrors.removeAll(amountErrors)
dispatch(AmountAction.SetAmountError(createValidateTransactionError(amountErrors, walletManager)))
}
transactionErrors.removeAll(amountErrors)
if (transactionErrors.isNotEmpty()) {
dispatch(SendAction.SendError(createValidateTransactionError(transactionErrors, walletManager)))
}
@ -84,7 +84,7 @@ class AmountMiddleware {
dispatch(AmountAction.SetAmount(sendState.amountState.balanceCrypto, false))
if (sendState.amountState.isCoinAmount()) {
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true, chipGroup = true))
dispatch(FeeAction.ChangeLayoutVisibility(main = true, controls = true))
dispatch(FeeActionUi.ChangeIncludeFee(true))
}

View file

@ -29,7 +29,7 @@ class RequestFeeMiddleware {
if (!SendState.isReadyToRequestFee()) {
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.ADDRESS_OR_AMOUNT_IS_EMPTY))
dispatch(FeeAction.ChangeLayoutVisibility(main = false, chipGroup = true))
// dispatch(FeeAction.ChangeLayoutVisibility(main = false, chipGroup = true))
dispatch(ReceiptAction.RefreshReceipt)
dispatch(SendAction.ChangeSendButtonState(sendState.getButtonState()))
return
@ -60,7 +60,7 @@ class RequestFeeMiddleware {
}
is Result.Failure -> {
dispatch(FeeAction.FeeCalculation.SetFeeError(FeeAction.Error.REQUEST_FAILED))
dispatch(FeeAction.ChangeLayoutVisibility(main = false, controls = false, chipGroup = false))
dispatch(FeeAction.ChangeLayoutVisibility(main = false))
}
}
dispatch(AmountActionUi.CheckAmountToSend)

View file

@ -1,16 +1,17 @@
package com.tangem.tap.features.send.redux.middlewares
import com.google.firebase.crashlytics.FirebaseCrashlytics
import com.tangem.blockchain.blockchains.binance.BinanceTransactionExtras
import com.tangem.blockchain.blockchains.stellar.StellarTransactionExtras
import com.tangem.blockchain.blockchains.xrp.XrpTransactionBuilder
import com.tangem.blockchain.common.*
import com.tangem.blockchain.extensions.SimpleResult
import com.tangem.common.card.Card
import com.tangem.common.core.TangemSdkError
import com.tangem.common.services.Result
import com.tangem.tap.common.analytics.AnalyticsEvent
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.dispatchOnMain
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.common.redux.navigation.NavigationAction
@ -57,12 +58,12 @@ class SendMiddleware {
val transactionData = appState()?.sendState?.externalTransactionData
if (transactionData != null) {
store.dispatchOnMain(AddressPayIdVerifyAction.AddressVerification.SetWalletAddress(
transactionData.destinationAddress, false
transactionData.destinationAddress, false
))
store.dispatchOnMain(AmountActionUi.SetMainCurrency(MainCurrencyType.CRYPTO))
store.dispatchOnMain(AmountActionUi.HandleUserInput(transactionData.amount))
store.dispatchOnMain(AmountAction.SetAmount(transactionData.amount.toBigDecimal(),
false))
false))
}
}
}
@ -72,8 +73,9 @@ class SendMiddleware {
}
}
private fun verifyAndSendTransaction(
action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit,
action: SendActionUi.SendAmountToRecipient, appState: AppState?, dispatch: (Action) -> Unit,
) {
val sendState = appState?.sendState ?: return
val walletManager = sendState.walletManager ?: return
@ -105,30 +107,47 @@ private fun verifyAndSendTransaction(
}
else -> {
sendTransaction(action, walletManager, amountToSend, feeAmount, destinationAddress,
sendState.transactionExtrasState, card, sendState.externalTransactionData, dispatch)
sendState.transactionExtrasState, card, sendState.externalTransactionData, dispatch)
}
}
}
private fun sendTransaction(
action: SendActionUi.SendAmountToRecipient,
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
destinationAddress: String,
transactionExtras: TransactionExtrasState,
card: Card,
externalTransactionData: ExternalTransactionData?,
dispatch: (Action) -> Unit,
action: SendActionUi.SendAmountToRecipient,
walletManager: WalletManager,
amountToSend: Amount,
feeAmount: Amount,
destinationAddress: String,
transactionExtras: TransactionExtrasState,
card: Card,
externalTransactionData: ExternalTransactionData?,
dispatch: (Action) -> Unit,
) {
dispatch(SendAction.ChangeSendButtonState(ButtonState.PROGRESS))
var txData = walletManager.createTransaction(amountToSend, feeAmount, destinationAddress)
transactionExtras.xlmMemo?.memo?.let { txData = txData.copy(extras = StellarTransactionExtras(it)) }
transactionExtras.binanceMemo?.memo?.let { txData = txData.copy(extras = BinanceTransactionExtras(it.toString())) }
transactionExtras.xrpDestinationTag?.tag?.let { txData = txData.copy(extras = XrpTransactionBuilder.XrpTransactionExtras(it)) }
scope.launch {
walletManager.update()
val updateWalletResult = walletManager.safeUpdate()
if (updateWalletResult is Result.Failure) {
when (val error = updateWalletResult.error) {
is TapError -> store.dispatchErrorNotification(error)
else -> {
val tapError = if (error.message == null) {
TapError.UnknownError
} else {
TapError.CustomError(error.message!!)
}
store.dispatchErrorNotification(tapError)
}
}
withMainContext { dispatch(SendAction.ChangeSendButtonState(ButtonState.ENABLED)) }
return@launch
}
val isLinkedTerminal = tangemSdk.config.linkedTerminal
if (card.isStart2Coin) {
tangemSdk.config.linkedTerminal = false
@ -146,9 +165,9 @@ private fun sendTransaction(
}
val result = (walletManager as TransactionSender).send(txData, signer)
withContext(Dispatchers.Main) {
tangemSdk.config.linkedTerminal = isLinkedTerminal
when (result) {
is SimpleResult.Success -> {
tangemSdk.config.linkedTerminal = isLinkedTerminal
FirebaseAnalyticsHandler.triggerEvent(
event = AnalyticsEvent.TRANSACTION_IS_SENT,
card = card,
@ -254,7 +273,7 @@ fun extractErrorsForAmountField(errors: EnumSet<TransactionError>): EnumSet<Tran
showIntoAmountField.add(it)
}
TransactionError.TotalExceedsBalance -> {
val notAcceptable = listOf(TransactionError.FeeExceedsBalance, TransactionError.FeeExceedsBalance)
val notAcceptable = listOf(TransactionError.AmountExceedsBalance, TransactionError.FeeExceedsBalance)
if (!showIntoAmountField.containsAll(notAcceptable)) showIntoAmountField.add(it)
}
TransactionError.InvalidAmountValue -> showIntoAmountField.add(it)

View file

@ -45,7 +45,7 @@ class FeeReducer : SendInternalReducer {
state.copy(
mainLayoutIsVisible = getVisibility(state.mainLayoutIsVisible, action.main),
controlsLayoutIsVisible = getVisibility(state.controlsLayoutIsVisible, action.controls),
feeChipGroupIsVisible = getVisibility(state.mainLayoutIsVisible, action.chipGroup)
feeChipGroupIsVisible = getVisibility(state.feeChipGroupIsVisible, action.chipGroup)
)
}
is FeeAction.FeeCalculation.SetFeeResult -> {

View file

@ -4,7 +4,6 @@ import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.extensions.scaleToFiat
import com.tangem.tap.common.extensions.stripZeroPlainString
import com.tangem.tap.domain.getFirstToken
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.*
@ -40,15 +39,15 @@ class ReceiptReducer : SendInternalReducer {
val wallet = sendState.walletManager?.wallet ?: return sendState
val layoutType = determineLayoutType(amountState.mainCurrency.type, amountState.typeOfAmount)
val symbols = determineSymbols(wallet)
val symbols = determineSymbols(wallet, amountState.typeOfAmount)
val showBlank = !SendState.isReadyToSend()
val result = state.copy(
visibleTypeOfReceipt = layoutType,
mainCurrency = amountState.mainCurrency,
fiat = createFiatType(symbols, showBlank),
crypto = createCryptoType(symbols, showBlank),
tokenFiat = createTokenFiatType(symbols, showBlank),
tokenCrypto = createTokenCryptoType(symbols, showBlank)
visibleTypeOfReceipt = layoutType,
mainCurrency = amountState.mainCurrency,
fiat = createFiatType(symbols, showBlank),
crypto = createCryptoType(symbols, showBlank),
tokenFiat = createTokenFiatType(symbols, showBlank),
tokenCrypto = createTokenCryptoType(symbols, showBlank)
)
return updateLastState(sendState.copy(receiptState = result), result)
}
@ -65,22 +64,22 @@ class ReceiptReducer : SendInternalReducer {
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto.minus(feeCrypto))
val totalFiat = convertToFiatPrecision(amountState.amountToSendCrypto)
ReceiptFiat(
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
symbols = symbols
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
symbols = symbols
)
} else {
val totalAmountCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
val amountFiat = convertToFiatPrecision(amountState.amountToSendCrypto)
val totalFiat = convertToFiatPrecision(totalAmountCrypto)
ReceiptFiat(
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = totalAmountCrypto.stripZeroPlainString(),
symbols = symbols
amountFiat = amountFiat,
feeFiat = feeFiat,
totalFiat = totalFiat,
willSentCrypto = totalAmountCrypto.stripZeroPlainString(),
symbols = symbols
)
}
}
@ -94,22 +93,22 @@ class ReceiptReducer : SendInternalReducer {
if (feeState.feeIsIncluded) {
return ReceiptCrypto(
amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString(),
totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(amountState.amountToSendCrypto),
symbols = symbols
amountCrypto = amountState.amountToSendCrypto.minus(feeCrypto).stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString(),
totalCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(amountState.amountToSendCrypto),
symbols = symbols
)
} else {
val totalCrypto = amountState.amountToSendCrypto.plus(feeCrypto)
return ReceiptCrypto(
amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString(),
totalCrypto = totalCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(totalCrypto),
symbols = symbols
amountCrypto = amountState.amountToSendCrypto.stripZeroPlainString(),
feeCrypto = feeCrypto.stripZeroPlainString(),
totalCrypto = totalCrypto.stripZeroPlainString(),
feeFiat = feeFiat,
willSentFiat = convertToFiatPrecision(totalCrypto),
symbols = symbols
)
}
}
@ -128,21 +127,21 @@ class ReceiptReducer : SendInternalReducer {
val amountFiat = sendState.tokenConverter!!.toFiatUnscaled(tokensToSend)
val totalFiat = amountFiat.plus(feeFiat)
ReceiptTokenFiat(
amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(),
feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols
amountFiat = amountFiat.scaleToFiat(true).stripZeroPlainString(),
feeFiat = feeFiat.scaleToFiat(true).stripZeroPlainString(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols
)
} else {
ReceiptTokenFiat(
amountFiat = EMPTY,
feeFiat = EMPTY,
totalFiat = EMPTY,
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols
amountFiat = EMPTY,
feeFiat = EMPTY,
totalFiat = EMPTY,
willSentToken = tokensToSend.stripZeroPlainString(),
willSentFeeCoin = feeCoin.stripZeroPlainString(),
symbols = symbols
)
}
}
@ -160,27 +159,30 @@ class ReceiptReducer : SendInternalReducer {
val totalFiat = tokenFiat.plus(feeFiat)
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString(),
totalFiat = totalFiat.scaleToFiat(true).stripZeroPlainString(),
symbols = symbols
)
} else {
ReceiptTokenCrypto(
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString(),
totalFiat = EMPTY,
symbols = symbols
amountToken = tokensToSend.stripZeroPlainString(),
feeCoin = feeCoin.stripZeroPlainString(),
totalFiat = EMPTY,
symbols = symbols
)
}
}
private fun determineSymbols(wallet: Wallet): ReceiptSymbols {
private fun determineSymbols(wallet: Wallet, amountType: AmountType): ReceiptSymbols {
return ReceiptSymbols(
fiat = store.state.globalState.appCurrency,
crypto = wallet.blockchain.currency,
token = wallet.getFirstToken()?.symbol
fiat = store.state.globalState.appCurrency,
crypto = wallet.blockchain.currency,
token = when (amountType) {
is AmountType.Token -> amountType.token.symbol
else -> null
}
)
}

View file

@ -14,7 +14,8 @@ class TransactionExtrasReducer : SendInternalReducer {
return when (action) {
is Prepare -> handleInitialization(action, sendState)
Release -> handleRelease(action, sendState)
is XlmMemo -> handleMemo(action, sendState, sendState.transactionExtrasState)
is XlmMemo -> handleXlmMemo(action, sendState, sendState.transactionExtrasState)
is BinanceMemo -> handleBinanceMemo(action, sendState, sendState.transactionExtrasState)
is XrpDestinationTag -> handleXrpTag(action, sendState, sendState.transactionExtrasState)
else -> sendState
}
@ -40,6 +41,7 @@ class TransactionExtrasReducer : SendInternalReducer {
}
}
Blockchain.Stellar -> TransactionExtrasState(xlmMemo = XlmMemoState())
Blockchain.Binance -> TransactionExtrasState(binanceMemo = BinanceMemoState())
else -> emptyResult
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
@ -50,7 +52,7 @@ class TransactionExtrasReducer : SendInternalReducer {
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleMemo(
private fun handleXlmMemo(
action: XlmMemo,
sendState: SendState,
infoState: TransactionExtrasState,
@ -94,6 +96,26 @@ class TransactionExtrasReducer : SendInternalReducer {
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleBinanceMemo(
action: BinanceMemo,
sendState: SendState,
infoState: TransactionExtrasState,
): SendState {
val result = when (action) {
is BinanceMemo.HandleUserInput -> {
val tag = action.data.toBigIntegerOrNull()
if (tag != null) {
val input = InputViewValue(action.data, true)
val tagState = BinanceMemoState(input, tag)
infoState.copy(binanceMemo = tagState)
} else {
infoState
}
}
}
return updateLastState(sendState.copy(transactionExtrasState = result), result)
}
private fun handleXrpTag(
action: XrpDestinationTag,
sendState: SendState,

View file

@ -27,6 +27,7 @@ data class AddressPayIdState(
data class TransactionExtrasState(
val xlmMemo: XlmMemoState? = null,
val binanceMemo: BinanceMemoState? = null,
val xrpDestinationTag: XrpDestinationTagState? = null
) : IdStateHolder {
override val stateId: StateId = StateId.TRANSACTION_EXTRAS
@ -54,6 +55,16 @@ data class XlmMemoState(
}
}
data class BinanceMemoState(
val viewFieldValue: InputViewValue = InputViewValue(""),
val memo: BigInteger? = null,
val error: TransactionExtraError? = null
) {
companion object {
val MAX_NUMBER: BigInteger = BigInteger("FFFFFFFFFFFFFFFF", 16)
}
}
// tag must contains only digits
data class XrpDestinationTagState(
val viewFieldValue: InputViewValue = InputViewValue(""),
@ -67,5 +78,6 @@ data class XrpDestinationTagState(
enum class TransactionExtraError {
INVALID_DESTINATION_TAG,
INVALID_XLM_MEMO
INVALID_XLM_MEMO,
INVALID_BINANCE_MEMO
}

View file

@ -115,7 +115,7 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
private fun setupTransactionExtrasLayout() {
etMemo.inputtedTextAsFlow()
etXlmMemo.inputtedTextAsFlow()
.debounce(400)
.filter {
val info = store.state.sendState.transactionExtrasState
@ -132,6 +132,15 @@ class SendFragment : BaseStoreFragment(R.layout.fragment_send) {
}
.onEach { store.dispatch(TransactionExtrasAction.XrpDestinationTag.HandleUserInput(it)) }
.launchIn(mainScope)
etBinanceMemo.inputtedTextAsFlow()
.debounce(400)
.filter {
val info = store.state.sendState.transactionExtrasState
info.binanceMemo?.viewFieldValue?.value != it
}
.onEach { store.dispatch(TransactionExtrasAction.BinanceMemo.HandleUserInput(it)) }
.launchIn(mainScope)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

View file

@ -5,7 +5,6 @@ import androidx.appcompat.app.AlertDialog
import com.tangem.tap.common.redux.global.GlobalAction
import com.tangem.tap.features.feedback.SendTransactionFailedEmail
import com.tangem.tap.features.send.redux.SendAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -23,7 +22,7 @@ class SendTransactionFailsDialog {
store.dispatch(GlobalAction.SendFeedback(SendTransactionFailedEmail(dialog.errorMessage)))
}
setPositiveButton(R.string.common_no) { _, _ -> }
setOnDismissListener { store.dispatch(WalletAction.HideDialog) }
setOnDismissListener { store.dispatch(SendAction.Dialog.Hide) }
}.create()
}
}

View file

@ -7,6 +7,7 @@ import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup
import androidx.core.text.bold
import com.tangem.common.extensions.remove
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.getMessageString
import com.tangem.tap.common.text.DecimalDigitsInputFilter
@ -64,19 +65,20 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
}
showView(fg.xlmMemoContainer, infoState.xlmMemo)
showView(fg.xrpDestinationTagContainer, infoState.xrpDestinationTag)
showView(fg.binanceMemoContainer, infoState.binanceMemo)
infoState.xlmMemo?.let {
fg.etMemo.inputType = when (it.selectedMemoType) {
fg.etXlmMemo.inputType = when (it.selectedMemoType) {
XlmMemoType.TEXT -> InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS
XlmMemoType.ID -> InputType.TYPE_CLASS_NUMBER
}
if (!it.viewFieldValue.isFromUserInput) fg.etMemo.setText(it.viewFieldValue.value)
if (!it.viewFieldValue.isFromUserInput) fg.etXlmMemo.setText(it.viewFieldValue.value)
if (it.error != null) {
if (it.error == TransactionExtraError.INVALID_XLM_MEMO) {
fg.tilMemo.error = fg.getText(R.string.send_error_invalid_memo_id)
fg.tilXlmMemo.error = fg.getText(R.string.send_error_invalid_memo_id)
}
} else {
fg.tilMemo.error = null
fg.tilXlmMemo.error = null
}
}
infoState.xrpDestinationTag?.let {
@ -91,6 +93,18 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
fg.etDestinationTag.setText(it.viewFieldValue.value)
}
}
infoState.binanceMemo?.let {
if (infoState.binanceMemo.error != null) {
if (infoState.binanceMemo.error == TransactionExtraError.INVALID_BINANCE_MEMO) {
fg.tilBinanceMemo.error = fg.getText(R.string.send_error_invalid_memo_id)
}
} else {
fg.tilBinanceMemo.error = null
}
if (!it.viewFieldValue.isFromUserInput) {
fg.etBinanceMemo.setText(it.viewFieldValue.value)
}
}
}
private fun handleSendScreen(fg: BaseStoreFragment, state: SendState) {
@ -187,9 +201,13 @@ class SendStateSubscriber(fragment: BaseStoreFragment) : FragmentStateSubscriber
fg.tvAmountCurrency.update(state.mainCurrency.currencySymbol)
(fg as? SendFragment)?.saveMainCurrency(state.mainCurrency.type)
val balanceText = fg.getString(R.string.send_balance_subtitle_format,
state.mainCurrency.currencySymbol,
state.viewBalanceValue)
val balanceText = when (state.mainCurrency.type) {
MainCurrencyType.FIAT -> fg.getString(R.string.send_balance_subtitle_format,
state.viewBalanceValue, state.mainCurrency.currencySymbol).remove(":")
MainCurrencyType.CRYPTO -> fg.getString(R.string.send_balance_subtitle_format,
state.mainCurrency.currencySymbol, state.viewBalanceValue)
}
fg.tvBalance.update(balanceText)
fg.tilAmountToSend.isEnabled = state.inputIsEnabled

View file

@ -0,0 +1,66 @@
package com.tangem.tap.features.tokens.redux
import androidx.annotation.StringRes
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.wallet.R
sealed class CurrencyListItem {
data class TokenListItem(val token: Token) : CurrencyListItem()
data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem()
data class TitleListItem(
@StringRes val titleResId: Int,
var isContentShown: Boolean = true,
val blockchain: Blockchain? = null,
) : CurrencyListItem()
companion object {
fun createListOfCurrencies(
blockchains: List<Blockchain>,
tokens: List<Token>,
): List<CurrencyListItem> {
val blockchainsTitle = R.string.add_tokens_subtitle_blockchains
val ethereumTokensTitle = R.string.add_tokens_subtitle_ethereum_tokens
val bscTokensTitle = R.string.add_tokens_subtitle_bsc_tokens
val binanceTokensTitle = R.string.add_tokens_subtitle_binance_tokens
val ethereumTokens = tokens.filter { it.blockchain == Blockchain.Ethereum }
val bscTokens = tokens.filter { it.blockchain == Blockchain.BSC }
val binanceTokens = tokens.filter { it.blockchain == Blockchain.Binance }
return listOf(TitleListItem(blockchainsTitle)) +
blockchains.map { BlockchainListItem(it) } +
listOf(TitleListItem(ethereumTokensTitle, blockchain = Blockchain.Ethereum)) +
ethereumTokens.map { TokenListItem(it) } +
listOf(TitleListItem(bscTokensTitle, blockchain = Blockchain.BSC)) +
bscTokens.map { TokenListItem(it) } +
listOf(TitleListItem(binanceTokensTitle, blockchain = Blockchain.Binance)) +
binanceTokens.map { TokenListItem(it) }
}
}
}
fun List<CurrencyListItem>.removeTokensForBlockchain(blockchain: Blockchain): List<CurrencyListItem> {
return filterNot {
it is CurrencyListItem.TokenListItem && it.token.blockchain == blockchain
}
}
fun List<CurrencyListItem>.addTokensForBlockchain(
blockchain: Blockchain, fullCurrenciesList: List<CurrencyListItem>,
): List<CurrencyListItem> {
val tokensToAdd = fullCurrenciesList.filter {
it is CurrencyListItem.TokenListItem && it.token.blockchain == blockchain
}
val indexToInsert = indexOfFirst {
it is CurrencyListItem.TitleListItem && it.blockchain == blockchain
} + 1
return this.toMutableList().apply { addAll(indexToInsert, tokensToAdd) }
}
fun List<CurrencyListItem>.toggleHeaderContentShownValue(blockchain: Blockchain) {
map {
if (it is CurrencyListItem.TitleListItem && it.blockchain == blockchain) {
it.isContentShown = !it.isContentShown
}
}
}

View file

@ -1,7 +1,7 @@
package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem
import com.tangem.tap.features.wallet.redux.WalletData
import org.rekotlin.Action
@ -17,5 +17,6 @@ sealed class TokensAction : Action {
data class SetAddedCurrencies(val wallets: List<WalletData>) : TokensAction()
data class ToggleShowTokensForBlockchain(val isShown: Boolean, val blockchain: Blockchain) : TokensAction()
}

View file

@ -3,7 +3,6 @@ package com.tangem.tap.features.tokens.redux
import com.tangem.tap.common.redux.AppState
import com.tangem.tap.currenciesRepository
import com.tangem.tap.domain.TapWorkarounds.isTestCard
import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem
import com.tangem.tap.store
import org.rekotlin.Middleware

View file

@ -18,7 +18,7 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
val tokensState = state.tokensState
return when (action) {
is TokensAction.LoadCurrencies.Success -> {
tokensState.copy(currencies = action.currencies)
tokensState.copy(currencies = action.currencies, shownCurrencies = action.currencies)
}
is TokensAction.SetAddedCurrencies -> {
tokensState.copy(addedCurrencies = action.wallets.toCardCurrencies())
@ -28,12 +28,26 @@ private fun internalReduce(action: Action, state: AppState): TokensState {
action.tokens.map { TokenWithAmount(it, null) }
))
}
is TokensAction.ToggleShowTokensForBlockchain -> {
if (action.isShown) {
val shownCurrencies = tokensState.shownCurrencies
.removeTokensForBlockchain(action.blockchain)
shownCurrencies.toggleHeaderContentShownValue(action.blockchain)
tokensState.copy(shownCurrencies = shownCurrencies)
} else {
val shownCurrencies = tokensState.shownCurrencies
.addTokensForBlockchain(action.blockchain, tokensState.currencies)
shownCurrencies.toggleHeaderContentShownValue(action.blockchain)
tokensState.copy(shownCurrencies = shownCurrencies)
}
}
else -> tokensState
}
}
private fun List<WalletData>.toCardCurrencies(): CardCurrencies {
val tokens = mapNotNull { (it.currency as? Currency.Token)?.token }.toSet()
val blockchains = mapNotNull { (it.currency as? Currency.Blockchain)?.blockchain }.toSet()
val tokens = mapNotNull { (it.currency as? Currency.Token)?.token }.distinct()
val blockchains = mapNotNull { (it.currency as? Currency.Blockchain)?.blockchain }.distinct()
return CardCurrencies(tokens = tokens, blockchains = blockchains)
}

View file

@ -2,15 +2,14 @@ package com.tangem.tap.features.tokens.redux
import com.tangem.blockchain.common.Amount
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.features.tokens.ui.adapters.CurrencyListItem
import org.rekotlin.StateType
data class TokensState(
val addedTokens: LinkedHashSet<TokenWithAmount> = LinkedHashSet(),
val addedCurrencies: CardCurrencies? = null,
val currencies: List<CurrencyListItem> = emptyList(),
val shownCurrencies: List<CurrencyListItem> = emptyList(),
) : StateType
data class TokenWithAmount(

View file

@ -73,7 +73,7 @@ class AddTokensFragment : Fragment(R.layout.fragment_add_tokens),
override fun newState(state: TokensState) {
if (activity == null) return
viewAdapter.addedCurrencies = state.addedCurrencies
viewAdapter.submitUnfilteredList(state.currencies)
viewAdapter.submitUnfilteredList(state.shownCurrencies)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {

View file

@ -3,18 +3,17 @@ package com.tangem.tap.features.tokens.ui.adapters
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Token
import com.tangem.tap.common.extensions.getString
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.loadCurrenciesIcon
import com.tangem.tap.common.extensions.show
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.features.tokens.redux.CurrencyListItem
import com.tangem.tap.features.tokens.redux.TokensAction
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
@ -68,11 +67,11 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
object DiffUtilCallback : DiffUtil.ItemCallback<CurrencyListItem>() {
override fun areContentsTheSame(
oldItem: CurrencyListItem, newItem: CurrencyListItem
oldItem: CurrencyListItem, newItem: CurrencyListItem,
) = oldItem == newItem
override fun areItemsTheSame(
oldItem: CurrencyListItem, newItem: CurrencyListItem
oldItem: CurrencyListItem, newItem: CurrencyListItem,
) = oldItem == newItem
}
@ -161,38 +160,21 @@ class CurrenciesAdapter : ListAdapter<CurrencyListItem, RecyclerView.ViewHolder>
class TitleViewHolder(val view: View) :
RecyclerView.ViewHolder(view) {
fun bind(title: CurrencyListItem.TitleListItem) {
view.tv_subtitle.text = view.getString(title.titleResId).toUpperCase(Locale.US)
}
}
}
sealed class CurrencyListItem {
data class TokenListItem(val token: Token) : CurrencyListItem()
data class BlockchainListItem(val blockchain: Blockchain) : CurrencyListItem()
data class TitleListItem(@StringRes val titleResId: Int) : CurrencyListItem()
companion object {
fun createListOfCurrencies(
blockchains: List<Blockchain>,
tokens: List<Token>
): List<CurrencyListItem> {
val blockchainsTitle = R.string.add_tokens_subtitle_blockchains
val ethereumTokensTitle = R.string.add_tokens_subtitle_ethereum_tokens
val bscTokensTitle = R.string.add_tokens_subtitle_bsc_tokens
val binanceTokensTitle = R.string.add_tokens_subtitle_binance_tokens
val ethereumTokens = tokens.filter { it.blockchain == Blockchain.Ethereum }
val bscTokens = tokens.filter { it.blockchain == Blockchain.BSC }
val binanceTokens = tokens.filter { it.blockchain == Blockchain.Binance }
return listOf(TitleListItem(blockchainsTitle)) +
blockchains.map { BlockchainListItem(it) } +
listOf(TitleListItem(ethereumTokensTitle)) +
ethereumTokens.map { TokenListItem(it) } +
listOf(TitleListItem(bscTokensTitle)) +
bscTokens.map { TokenListItem(it) } +
listOf(TitleListItem(binanceTokensTitle)) +
binanceTokens.map { TokenListItem(it) }
view.tv_subtitle.text = view.getString(title.titleResId).uppercase()
if (title.blockchain != null) {
view.cl_subtitle_container.setOnClickListener {
val rotation = if (title.isContentShown) -90f else 0f
store.dispatch(TokensAction.ToggleShowTokensForBlockchain(
title.isContentShown, title.blockchain
))
view.iv_toggle_sublist_visibility.animate().rotation(rotation)
}
view.iv_toggle_sublist_visibility.show()
view.iv_toggle_sublist_visibility.setImageResource(R.drawable.ic_arrow_angle_down)
} else {
view.iv_toggle_sublist_visibility.hide()
}
}
}
}

View file

@ -9,6 +9,7 @@ import com.tangem.tap.common.redux.NotificationAction
import com.tangem.tap.domain.TapError
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.tokens.CardCurrencies
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.wallet.R
import org.rekotlin.Action
import java.math.BigDecimal
@ -25,7 +26,7 @@ sealed class WalletAction : Action {
data class LoadWallet(
val allowToSell: Boolean? = null, val allowToBuy: Boolean? = null,
val moonpayStatus: MoonpayStatus? = null,
val blockchain: Blockchain? = null,
) :
WalletAction() {

View file

@ -10,14 +10,16 @@ import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.common.toggleWidget.WidgetState
import com.tangem.tap.domain.configurable.warningMessage.WarningMessage
import com.tangem.tap.domain.extensions.buyIsAllowed
import com.tangem.tap.domain.extensions.sellIsAllowed
import com.tangem.tap.domain.extensions.toSendableAmounts
import com.tangem.tap.domain.topup.TradeCryptoHelper
import com.tangem.tap.features.onboarding.products.twins.redux.TwinCardsState
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactionsForToken
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.network.moonpay.MoonpayStatus
import com.tangem.tap.store
import org.rekotlin.StateType
import java.math.BigDecimal
@ -38,7 +40,6 @@ data class WalletState(
val primaryBlockchain: Blockchain? = null,
val primaryToken: Token? = null,
val isTestnet: Boolean = false,
val tradeCryptoAllowed: TradeCryptoAvailability = TradeCryptoAvailability()
) : StateType {
// if you do not delegate - the application crashes on startup,
@ -132,12 +133,39 @@ data class WalletState(
fun replaceWalletInWallets(walletData: WalletData?): List<WalletData> {
if (walletData == null) return wallets
return wallets.filter { it.currencyData.currency != walletData.currencyData.currency } + walletData
var changed = false
val updatedWallets = wallets.map {
if (it.currency == walletData.currency) {
changed = true
walletData
} else {
it
}
}
return if (changed) updatedWallets else wallets + walletData
}
fun replaceSomeWallets(newWallets: List<WalletData>): List<WalletData> {
val currencies = newWallets.map { it.currencyData.currency }
return wallets.filter { !currencies.contains(it.currencyData.currency) } + newWallets
val remainingWallets: MutableList<WalletData> = newWallets.toMutableList()
val updatedWallets = wallets.map { wallet ->
val newWallet = newWallets
.firstOrNull { wallet.currency == it.currency }
if (newWallet == null) {
wallet
} else {
remainingWallets.remove(newWallet)
newWallet
}
}
return updatedWallets + remainingWallets
}
fun updateTradeCryptoState(moonpayStatus: MoonpayStatus?, walletData: WalletData): WalletData {
return walletData.copy(tradeCryptoState = TradeCryptoState.from(moonpayStatus, walletData))
}
fun updateTradeCryptoState(moonpayStatus: MoonpayStatus?, walletDataList: List<WalletData>): List<WalletData> {
return walletDataList.map { it.copy(tradeCryptoState = TradeCryptoState.from(moonpayStatus, it)) }
}
fun addWalletManagers(newWalletManagers: List<WalletManager>): WalletState {
@ -148,10 +176,6 @@ data class WalletState(
}
sealed class WalletDialog : StateDialog {
data class QrDialog(
val qrCode: Bitmap?, val shareUrl: String?, val currencyName: CryptoCurrencyName?
) : WalletDialog()
data class SelectAmountToSendDialog(val amounts: List<Amount>?) : WalletDialog()
object SignedHashesMultiWalletDialog : WalletDialog()
object ChooseTradeActionDialog : WalletDialog()
@ -200,13 +224,16 @@ data class Artwork(
data class TradeCryptoState(
val sellingAllowed: Boolean = false,
val buyingAllowed: Boolean = false,
)
) {
companion object {
fun from(moonpayStatus: MoonpayStatus?, walletData: WalletData): TradeCryptoState {
val status = moonpayStatus ?: return walletData.tradeCryptoState
val currency = walletData.currency
data class TradeCryptoAvailability(
val sellingAllowed: Boolean = false,
val buyingAllowed: Boolean = false,
val availableToSell: Set<String> = TradeCryptoHelper.AVAILABLE_TO_SELL,
)
return TradeCryptoState(status.sellIsAllowed(currency), status.buyIsAllowed(currency))
}
}
}
data class WalletData(
val pendingTransactions: List<PendingTransaction> = emptyList(),
@ -219,7 +246,7 @@ data class WalletData(
val fiatRateString: String? = null,
val fiatRate: BigDecimal? = null,
val mainButton: WalletMainButton = WalletMainButton.SendButton(false),
val currency: Currency? = null
val currency: Currency
) {
fun shouldShowMultipleAddress(): Boolean {
val listOfAddresses = walletAddresses?.list ?: return false

View file

@ -54,7 +54,11 @@ class MultiWalletMiddleware {
}
}
store.dispatch(WalletAction.LoadFiatRate(currency = Currency.Blockchain(action.blockchain)))
store.dispatch(WalletAction.LoadWallet(blockchain = action.blockchain))
store.dispatch(WalletAction.LoadWallet(
moonpayStatus = globalState?.moonpayStatus,
blockchain = action.blockchain
)
)
}
is WalletAction.MultiWallet.SaveCurrencies -> {
val cardId = globalState?.scanResponse?.card?.cardId

View file

@ -12,6 +12,7 @@ import com.tangem.operations.attestation.OnlineCardVerifier
import com.tangem.tap.*
import com.tangem.tap.common.analytics.FirebaseAnalyticsHandler
import com.tangem.tap.common.extensions.*
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
@ -21,6 +22,8 @@ import com.tangem.tap.features.home.redux.HomeAction
import com.tangem.tap.features.send.redux.PrepareSendScreen
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.network.NetworkStateChanged
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch
import org.rekotlin.Action
import org.rekotlin.DispatchFunction
@ -51,9 +54,9 @@ class WalletMiddleware {
is WalletAction.LoadWallet -> {
scope.launch {
if (action.blockchain == null) {
walletState.walletManagers.map { walletManager ->
globalState.tapWalletManager.loadWalletData(walletManager)
}
walletState.walletManagers.map { walletManager ->
async { globalState.tapWalletManager.loadWalletData(walletManager) }
}.awaitAll()
} else {
val walletManager = walletState.getWalletManager(action.blockchain)
walletManager?.let { globalState.tapWalletManager.loadWalletData(it) }
@ -101,14 +104,14 @@ class WalletMiddleware {
when (result) {
is CompletionResult.Success -> {
val scanNoteResponse = globalState.scanResponse?.copy(card = result.data)
scanNoteResponse?.let { store.onCardScanned(scanNoteResponse) }
scanNoteResponse?.let { store.onCardScanned(scanNoteResponse, false) }
}
is CompletionResult.Failure -> {
(result.error as? TangemSdkError)?.let { error ->
FirebaseAnalyticsHandler.logCardSdkError(
error,
FirebaseAnalyticsHandler.ActionToLog.CreateWallet,
card = store.state.detailsState.card
card = store.state.detailsState.scanResponse?.card
)
}
}
@ -191,6 +194,13 @@ class WalletMiddleware {
store.dispatch(NavigationAction.NavigateTo(AppScreen.Send))
}
}
is WalletAction.ShowDialog.QrCode -> {
val selectedWalletData = walletState.getWalletData(walletState.selectedWallet) ?: return
val selectedAddressData = selectedWalletData.walletAddresses?.selectedAddress ?: return
val currency = selectedWalletData.currency
store.dispatchDialogShow(AppDialog.AddressInfoDialog(currency, selectedAddressData))
}
}
}

View file

@ -38,11 +38,6 @@ class MultiWalletReducer {
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Blockchain(blockchain),
tradeCryptoState = TradeCryptoState(
sellingAllowed = state.tradeCryptoAllowed.sellingAllowed &&
state.tradeCryptoAllowed.availableToSell.contains(blockchain.currency),
buyingAllowed = state.tradeCryptoAllowed.buyingAllowed
)
)
}
@ -67,11 +62,6 @@ class MultiWalletReducer {
walletAddresses = createAddressList(wallet),
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Blockchain(action.blockchain),
tradeCryptoState = TradeCryptoState(
sellingAllowed = state.tradeCryptoAllowed.sellingAllowed &&
state.tradeCryptoAllowed.availableToSell.contains(action.blockchain.currency),
buyingAllowed = state.tradeCryptoAllowed.buyingAllowed
)
)
val newState = state.copy(wallets = state.replaceWalletInWallets(walletData))
if (wallet != null && wallet.amounts[AmountType.Coin]?.value != null) {
@ -115,7 +105,8 @@ class MultiWalletReducer {
}
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
currency = Currency.Token(action.token)
)
val wallets = state.replaceWalletInWallets(newTokenWalletData)
state.copy(wallets = wallets)
@ -173,10 +164,5 @@ fun Token.toWallet(state: WalletState): WalletData? {
walletAddresses = walletAddresses,
mainButton = WalletMainButton.SendButton(false),
currency = Currency.Token(this),
tradeCryptoState = TradeCryptoState(
sellingAllowed = state.tradeCryptoAllowed.sellingAllowed &&
state.tradeCryptoAllowed.availableToSell.contains(this.symbol),
buyingAllowed = state.tradeCryptoAllowed.buyingAllowed
)
)
}

View file

@ -3,14 +3,14 @@ package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Wallet
import com.tangem.common.extensions.isZero
import com.tangem.tap.common.extensions.*
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.domain.getFirstToken
import com.tangem.tap.features.wallet.models.removeUnknownTransactions
import com.tangem.tap.features.wallet.models.toPendingTransactions
import com.tangem.tap.features.wallet.redux.ProgressState
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.*
import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.features.wallet.ui.BalanceWidgetData
import com.tangem.tap.features.wallet.ui.TokenData
@ -19,39 +19,39 @@ import java.math.RoundingMode
class OnWalletLoadedReducer {
fun reduce(wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null): WalletState {
fun reduce(wallet: Wallet, walletState: WalletState): WalletState {
return if (!walletState.isMultiwalletAllowed) {
onSingleWalletLoaded(wallet, walletState, topUpAllowed)
onSingleWalletLoaded(wallet, walletState)
} else {
onMultiWalletLoaded(wallet, walletState, topUpAllowed)
onMultiWalletLoaded(wallet, walletState)
}
}
private fun onMultiWalletLoaded(
wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
): WalletState {
private fun onMultiWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState {
val fiatCurrencySymbol = store.state.globalState.appCurrency
val amount = wallet.amounts[AmountType.Coin]?.value
val moonpayStatus = store.state.globalState.moonpayStatus
val coinAmountValue = wallet.amounts[AmountType.Coin]?.value
if (walletState.getWalletData(wallet.blockchain) == null) {
return walletState
}
val formattedAmount = amount?.toFormattedCurrencyString(
val formattedAmount = coinAmountValue?.toFormattedCurrencyString(
wallet.blockchain.decimals(),
wallet.blockchain.currency)
val pendingTransactions = wallet.recentTransactions
.toPendingTransactions(wallet.address)
val sendButtonEnabled = amount?.isZero() == false && pendingTransactions.isEmpty()
val coinSendButton = coinAmountValue?.isZero() == false && pendingTransactions.isEmpty()
val balanceStatus = if (pendingTransactions.isNotEmpty()) {
BalanceStatus.TransactionInProgress
} else {
BalanceStatus.VerifiedOnline
}
val walletData = walletState.getWalletData(wallet.blockchain)
?: WalletData()
val fiatAmount = walletData.fiatRate?.let { amount?.toFiatValue(it) }
val newWalletData = walletData.copy(
val fiatAmount = walletData?.fiatRate?.let { coinAmountValue?.toFiatValue(it) }
val newWalletData = walletData?.copy(
currencyData = walletData.currencyData.copy(
status = balanceStatus, currency = wallet.blockchain.fullName,
currencySymbol = wallet.blockchain.currency,
@ -60,7 +60,9 @@ class OnWalletLoadedReducer {
fiatAmountFormatted = fiatAmount?.toFormattedFiatValue(fiatCurrencySymbol)
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
mainButton = WalletMainButton.SendButton(coinSendButton),
currency = Currency.Blockchain(wallet.blockchain),
tradeCryptoState = TradeCryptoState.from(moonpayStatus, walletData)
)
val tokens = wallet.getTokens().mapNotNull { token ->
@ -71,23 +73,24 @@ class OnWalletLoadedReducer {
pendingTransactions.isNotEmpty() -> BalanceStatus.SameCurrencyTransactionInProgress
else -> BalanceStatus.VerifiedOnline
}
val tokenFiatAmount = tokenWalletData?.fiatRate?.let { rate ->
wallet.getTokenAmount(token)?.value?.toFiatValue(rate)
}
val tokenAmountValue = wallet.getTokenAmount(token)?.value
val tokenFiatAmount = tokenWalletData?.fiatRate?.let { rate -> tokenAmountValue?.toFiatValue(rate)}
val tokenSendButton = tokenAmountValue?.isZero() == false && tokenPendingTransactions.isEmpty()
tokenWalletData?.copy(
currencyData = tokenWalletData.currencyData.copy(
status = tokenBalanceStatus,
amount = wallet.getTokenAmount(token)?.value?.toFormattedCurrencyString(
token.decimals, token.symbol
),
amount = tokenAmountValue?.toFormattedCurrencyString(token.decimals, token.symbol),
fiatAmount = tokenFiatAmount,
fiatAmountFormatted = tokenFiatAmount?.toFormattedFiatValue(fiatCurrencySymbol)
),
pendingTransactions = tokenPendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
mainButton = WalletMainButton.SendButton(tokenSendButton),
tradeCryptoState = TradeCryptoState.from(moonpayStatus, tokenWalletData)
)
}
val wallets = walletState.replaceSomeWallets((tokens + newWalletData))
val newWallets = (tokens + newWalletData).mapNotNull { it }
val wallets = walletState.replaceSomeWallets((newWallets))
val state = if (wallets.any { it.currencyData.status == BalanceStatus.Loading }) {
ProgressState.Loading
@ -99,12 +102,12 @@ class OnWalletLoadedReducer {
)
}
private fun onSingleWalletLoaded(
wallet: Wallet, walletState: WalletState, topUpAllowed: Boolean? = null
): WalletState {
private fun onSingleWalletLoaded(wallet: Wallet, walletState: WalletState): WalletState {
if (wallet.blockchain != walletState.primaryBlockchain) return walletState
val fiatCurrencySymbol = store.state.globalState.appCurrency
val moonpayStatus = store.state.globalState.moonpayStatus
val token = wallet.getFirstToken()
val tokenData = if (token != null) {
val tokenAmount = wallet.getTokenAmount(token)
@ -150,7 +153,8 @@ class OnWalletLoadedReducer {
fiatAmount = fiatAmountRaw
),
pendingTransactions = pendingTransactions.removeUnknownTransactions(),
mainButton = WalletMainButton.SendButton(sendButtonEnabled)
mainButton = WalletMainButton.SendButton(sendButtonEnabled),
tradeCryptoState = TradeCryptoState.from(moonpayStatus, walletState.primaryWallet)
)
val wallets = walletData?.let { listOf(walletData) } ?: emptyList()
return walletState.copy(

View file

@ -1,8 +1,12 @@
package com.tangem.tap.features.wallet.redux.reducers
import com.tangem.blockchain.common.AmountType
import com.tangem.blockchain.common.Blockchain
import com.tangem.blockchain.common.Wallet
import com.tangem.tap.common.extensions.*
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.common.redux.global.FiatCurrencyName
import com.tangem.tap.domain.TapError
@ -12,6 +16,7 @@ import com.tangem.tap.domain.twins.TwinCardNumber
import com.tangem.tap.features.wallet.redux.*
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
import java.math.RoundingMode
@ -29,6 +34,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
if (action !is WalletAction) return state.walletState
val moonpayStatus = store.state.globalState.moonpayStatus
var newState = state.walletState
when (action) {
@ -44,6 +50,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.EmptyCard),
mainButton = WalletMainButton.CreateWalletButton(true),
currency = Currency.Blockchain(Blockchain.Unknown)
)
)
)
@ -71,6 +78,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
wallets = listOf(
WalletData(
currencyData = BalanceWidgetData(BalanceStatus.UnknownBlockchain),
currency = Currency.Blockchain(Blockchain.Unknown)
)
)
)
@ -95,28 +103,18 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
currencySymbol = wallet.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
tradeCryptoState = TradeCryptoState(
sellingAllowed = action.allowToSell ?: wallet.tradeCryptoState.sellingAllowed &&
state.walletState.tradeCryptoAllowed.availableToSell.contains(wallet.currencyData.currencySymbol),
buyingAllowed = action.allowToBuy
?: wallet.tradeCryptoState.buyingAllowed
)
tradeCryptoState = TradeCryptoState.from(action.moonpayStatus, wallet)
)
}
val tradeCryptoAllowed = TradeCryptoAvailability(
sellingAllowed = action.allowToSell ?: false,
buyingAllowed = action.allowToBuy ?: false
)
newState = newState.copy(
state = ProgressState.Loading,
wallets = wallets,
tradeCryptoAllowed = tradeCryptoAllowed
)
} else {
val walletManager = newState.getWalletManager(action.blockchain) ?: return newState
val blockchain = walletManager.wallet.blockchain
val currencies = listOf(Currency.Blockchain(blockchain)) +
walletManager.cardTokens.map { Currency.Token(it) }
walletManager.cardTokens.map { Currency.Token(it) }
val newWallets = newState.wallets.filter { currencies.contains(it.currency) }
.map { wallet ->
wallet.copy(
@ -126,16 +124,11 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
currencySymbol = wallet.currencyData.currencySymbol,
),
mainButton = WalletMainButton.SendButton(false),
tradeCryptoState = TradeCryptoState(
sellingAllowed = action.allowToSell ?: wallet.tradeCryptoState.sellingAllowed &&
state.walletState.tradeCryptoAllowed.availableToSell.contains(wallet.currencyData.currencySymbol),
buyingAllowed = action.allowToBuy
?: wallet.tradeCryptoState.buyingAllowed
)
tradeCryptoState = TradeCryptoState.from(action.moonpayStatus, wallet)
)
}
val wallets = newState.replaceSomeWallets(newWallets)
newState = newState.copy(wallets = wallets)
newState = newState.copy(wallets = newState.updateTradeCryptoState(moonpayStatus, wallets))
}
}
is WalletAction.LoadWallet.Success -> newState =
@ -160,7 +153,7 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
}
newState = newState.copy(
state = progressState,
wallets = wallets
wallets = newState.updateTradeCryptoState(moonpayStatus, wallets)
)
}
is WalletAction.LoadWallet.Failure -> {
@ -194,7 +187,8 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
ProgressState.Done
}
newState = newState.copy(
state = progressState, wallets = wallets
state = progressState,
wallets = newState.updateTradeCryptoState(moonpayStatus, wallets)
)
}
is WalletAction.SetArtworkId -> {
@ -210,23 +204,13 @@ private fun internalReduce(action: Action, state: AppState): WalletState {
newState = setNewFiatRate(action.fiatRate, state.globalState.appCurrency, newState)
is WalletAction.LoadArtwork -> {
val artworkUrl = action.card.getArtworkUrl(action.artworkId)
?: when (state.twinCardsState.cardNumber) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
else -> Artwork.DEFAULT_IMG_URL
}
?: when (state.twinCardsState.cardNumber) {
TwinCardNumber.First -> Artwork.TWIN_CARD_1
TwinCardNumber.Second -> Artwork.TWIN_CARD_2
else -> Artwork.DEFAULT_IMG_URL
}
newState = newState.copy(cardImage = Artwork(artworkId = artworkUrl))
}
is WalletAction.ShowDialog.QrCode -> {
val selectedWalletData = newState.getWalletData(newState.selectedWallet)
newState = newState.copy(
walletDialog = WalletDialog.QrDialog(
selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl?.toQrCode(),
selectedWalletData?.walletAddresses?.selectedAddress?.shareUrl,
selectedWalletData?.currencyData?.currency
)
)
}
is WalletAction.ShowDialog.SignedHashesMultiWalletDialog -> {
newState = newState.copy(walletDialog = WalletDialog.SignedHashesMultiWalletDialog)
}
@ -273,7 +257,7 @@ fun createAddressList(wallet: Wallet?, walletAddresses: WalletAddresses? = null)
var indexOfSelectedWallet = 0
walletAddresses?.let {
val index =
listOfAddressData.indexOfFirst { it.address == walletAddresses.selectedAddress.address }
listOfAddressData.indexOfFirst { it.address == walletAddresses.selectedAddress.address }
if (index != -1) indexOfSelectedWallet = index
}
return WalletAddresses(listOfAddressData[indexOfSelectedWallet], listOfAddressData)
@ -284,10 +268,10 @@ fun Wallet.createAddressesData(): List<AddressData> {
// put a defaultAddress at the first place
addresses.forEach {
val addressData = AddressData(
it.value,
it.type,
getShareUri(it.value),
getExploreUrl(it.value)
it.value,
it.type,
getShareUri(it.value),
getExploreUrl(it.value)
)
if (it.type == blockchain.defaultAddressType()) {
listOfAddressData.add(0, addressData)

View file

@ -23,14 +23,14 @@ class MultipleAddressUiHelper {
return when (id) {
R.id.chip_default -> {
when (blockchain) {
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Segwit
Blockchain.Bitcoin, Blockchain.BitcoinTestnet, Blockchain.Litecoin -> BitcoinAddressType.Segwit
Blockchain.CardanoShelley -> CardanoAddressType.Shelley
else -> null
}
}
R.id.chip_legacy -> {
when (blockchain) {
Blockchain.Bitcoin, Blockchain.BitcoinTestnet -> BitcoinAddressType.Legacy
Blockchain.Bitcoin, Blockchain.BitcoinTestnet, Blockchain.Litecoin -> BitcoinAddressType.Legacy
Blockchain.CardanoShelley -> CardanoAddressType.Byron
else -> null
}

View file

@ -14,6 +14,7 @@ import com.tangem.tap.common.SnackbarHandler
import com.tangem.tap.common.extensions.*
import com.tangem.tap.common.redux.StateDialog
import com.tangem.tap.common.redux.navigation.NavigationAction
import com.tangem.tap.features.onboarding.getQRReceiveMessage
import com.tangem.tap.features.wallet.models.PendingTransaction
import com.tangem.tap.features.wallet.redux.*
import com.tangem.tap.features.wallet.ui.adapters.PendingTransactionsAdapter
@ -84,7 +85,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
btn_share.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) }
btn_top_up.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Buy) }
btn_trade.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Buy) }
btn_sell.setOnClickListener { store.dispatch(WalletAction.TradeCryptoAction.Sell) }
}
@ -127,8 +128,6 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
srl_wallet_details.setOnRefreshListener {
if (selectedWallet.currencyData.status != BalanceStatus.Loading) {
store.dispatch(WalletAction.LoadWallet(
allowToBuy = selectedWallet.tradeCryptoState.buyingAllowed,
allowToSell = selectedWallet.tradeCryptoState.sellingAllowed,
blockchain = selectedWallet.currency?.blockchain
))
}
@ -138,7 +137,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
srl_wallet_details.isRefreshing = false
}
btn_top_up.isEnabled = selectedWallet.tradeCryptoState.buyingAllowed
btn_trade.isEnabled = selectedWallet.tradeCryptoState.buyingAllowed
btn_sell.show(selectedWallet.tradeCryptoState.sellingAllowed)
}
@ -146,7 +145,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
Picasso.get().loadCurrenciesIcon(
imageView = iv_currency,
textView = tv_token_letter,
blockchain = wallet.currency?.blockchain,
blockchain = wallet.currency.blockchain,
token = (wallet.currency as? Currency.Token)?.token
)
}
@ -182,6 +181,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
requireContext()))
}
iv_qr_code.setImageBitmap(state.walletAddresses.selectedAddress.shareUrl.toQrCode())
tv_recieve_message.text = getQRReceiveMessage(tv_recieve_message.context, state.currency)
}
}
@ -286,7 +286,7 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.meny_remove -> {
R.id.menu_remove -> {
store.state.walletState.getSelectedWalletData()?.let { walletData ->
store.dispatch(WalletAction.MultiWallet.RemoveWallet(walletData))
store.dispatch(WalletAction.MultiWallet.SelectWallet(null))
@ -300,9 +300,11 @@ class WalletDetailsFragment : Fragment(R.layout.fragment_wallet_details), StoreS
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
if (store.state.walletState.canBeRemoved(store.state.walletState.getSelectedWalletData())) {
inflater.inflate(R.menu.wallet_details, menu)
}
inflater.inflate(R.menu.wallet_details, menu)
val walletCanBeRemoved = store.state.walletState.canBeRemoved(
store.state.walletState.getSelectedWalletData()
)
menu.getItem(0).isEnabled = walletCanBeRemoved
}
}

View file

@ -18,7 +18,6 @@ import com.tangem.tap.features.wallet.ui.BalanceStatus
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.item_currency_wallet.view.*
import java.math.BigDecimal
class WalletAdapter
: ListAdapter<WalletData, WalletAdapter.WalletsViewHolder>(DiffUtilCallback) {
@ -28,36 +27,8 @@ class WalletAdapter
}
fun submitList(list: List<WalletData>, primaryBlockchain: Blockchain?, primaryToken: Token? = null) {
val listModified = list.toMutableList()
val primaryBlockchainWallet = when (
val index = listModified.indexOfFirst {
(it.currency as? Currency.Blockchain)?.blockchain == primaryBlockchain
}
) {
-1 -> null
else -> listModified.removeAt(index)
}
val primaryTokenWallet = if (primaryToken == null) null else when (
val index = listModified.indexOfFirst { (it.currency as? Currency.Token)?.token == primaryToken }
) {
-1 -> null
else -> listModified.removeAt(index)
}
if (list.all { it.currencyData.fiatAmountFormatted == null }) {
val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + listModified
super.submitList(sortedList)
return
}
val sorted = listModified.sortedWith(
compareByDescending<WalletData> { it.currencyData.fiatAmount ?: BigDecimal.ZERO }
.thenBy { it.currencyData.currency }
)
val sortedList = listOfNotNull(primaryBlockchainWallet, primaryTokenWallet) + sorted
super.submitList(sortedList)
// We used this method to sort the list of currencies. Sorting is disabled for now.
super.submitList(list)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WalletsViewHolder {
@ -92,7 +63,7 @@ class WalletAdapter
view.card_wallet.setOnClickListener {
store.dispatch(WalletAction.MultiWallet.SelectWallet(wallet))
}
val blockchain = wallet.currency?.blockchain
val blockchain = wallet.currency.blockchain
val token = (wallet.currency as? Currency.Token)?.token
Picasso.get().loadCurrenciesIcon(

View file

@ -1,36 +0,0 @@
package com.tangem.tap.features.wallet.ui.dialogs
import android.app.Dialog
import android.content.Context
import android.graphics.Bitmap
import com.tangem.tap.common.extensions.hide
import com.tangem.tap.common.extensions.show
import com.tangem.tap.common.redux.global.CryptoCurrencyName
import com.tangem.tap.features.wallet.redux.WalletAction
import com.tangem.tap.store
import com.tangem.wallet.R
import kotlinx.android.synthetic.main.dialog_qrcode.*
class QrDialog(context: Context) : Dialog(context) {
init {
this.setContentView(R.layout.dialog_qrcode)
}
fun showQr(qrCode: Bitmap, shareUrl: String, currencyName: CryptoCurrencyName?) {
this.setOnDismissListener { store.dispatch(WalletAction.HideDialog) }
this.btn_done?.setOnClickListener { store.dispatch(WalletAction.HideDialog) }
this.tv_qr_dialog_address?.text = shareUrl
this.iv_qrcode?.setImageBitmap(qrCode)
if (currencyName == null) {
this.tv_qr_dialog_header.hide()
} else {
this.tv_qr_dialog_header.show()
this.tv_qr_dialog_header.text = context.getString(
R.string.wallet_qr_title_format, currencyName
)
}
super.show()
}
}

View file

@ -21,7 +21,7 @@ class ScanFailsDialog {
setPositiveButton(R.string.alert_button_request_support) { _, _ ->
store.dispatch(GlobalAction.SendFeedback(ScanFailsEmail()))
}
setNegativeButton(R.string.common_cancel) { _, _ -> }
setNeutralButton(R.string.alert_troubleshooting_scan_card_ok) { _, _ -> }
setOnDismissListener { store.dispatchDialogHide() }
}.create()
}

View file

@ -140,14 +140,14 @@ class SingleWalletView : WalletView {
store.dispatch(WalletAction.CopyAddress(addressString, fragment.requireContext()))
}
}
btn_show_qr.setOnClickListener { store.dispatch(WalletAction.ShowDialog.QrCode) }
btn_top_up.setOnClickListener {
tradeCryptoAction(state.tradeCryptoState)
btn_show_qr.setOnClickListener {
store.dispatch(WalletAction.ShowDialog.QrCode)
}
setupTradeButton(fragment, state.tradeCryptoState)
}
private fun tradeCryptoAction(tradeCryptoState: TradeCryptoState) {
private fun setupTradeButton(fragment: WalletFragment, tradeCryptoState: TradeCryptoState) {
val allowedToBuy = tradeCryptoState.buyingAllowed
val allowedToSell = tradeCryptoState.sellingAllowed
val action = when {
@ -156,7 +156,23 @@ class SingleWalletView : WalletView {
allowedToBuy && allowedToSell -> WalletAction.ShowDialog.ChooseTradeActionDialog
else -> null
}
if (action != null) store.dispatch(action)
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_short_btn
!allowedToBuy && allowedToSell -> R.drawable.ic_arrow_down_short_button
allowedToBuy && allowedToSell -> R.drawable.ic_arrows_up_down_short_btn
else -> null
}
with(fragment) {
btn_trade.text = getText(text)
btn_trade.setCompoundDrawablesWithIntrinsicBounds(0, icon ?: 0, 0, 0)
btn_trade.setOnClickListener { if (action != null) store.dispatch(action) }
}
}
private fun setupButtonsType(state: WalletData, fragment: WalletFragment) = with(fragment) {
@ -228,15 +244,6 @@ class SingleWalletView : WalletView {
private fun handleDialogs(walletDialog: StateDialog?, fragment: WalletFragment) {
val context = fragment.context ?: return
when (walletDialog) {
is WalletDialog.QrDialog -> {
if (walletDialog.qrCode != null && walletDialog.shareUrl != null) {
if (dialog == null) dialog = QrDialog(context).apply {
this.showQr(
walletDialog.qrCode, walletDialog.shareUrl, walletDialog.currencyName
)
}
}
}
is WalletDialog.SelectAmountToSendDialog -> {
if (dialog == null) dialog = AmountToSendDialog(context).apply {
this.show(walletDialog.amounts)

View file

@ -4,13 +4,20 @@ import retrofit2.http.GET
import retrofit2.http.Query
interface MoonpayApi {
@GET( MOOONPAY_IP_ADDRESS_REQUEST_URL)
@GET(MOOONPAY_IP_ADDRESS_REQUEST_URL)
suspend fun getUserStatus(
@Query("apiKey") moonpayApiKey: String
@Query("apiKey") moonpayApiKey: String,
): MoonPayUserStatus
@GET(MOOONPAY_CURRENCIES_REQUEST_URL)
suspend fun getCurrencies(
@Query("apiKey") moonpayApiKey: String,
): List<MoonpayCurrencies>
companion object {
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/v4/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "ip_address/"
const val MOOONPAY_BASE_URL = "https://api.moonpay.com/"
const val MOOONPAY_IP_ADDRESS_REQUEST_URL = "v4/ip_address/"
const val MOOONPAY_CURRENCIES_REQUEST_URL = "v3/currencies/"
}
}

View file

@ -1,24 +1,90 @@
package com.tangem.tap.network.moonpay
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import com.tangem.common.services.Result
import com.tangem.common.services.performRequest
import com.tangem.tap.network.createRetrofitInstance
import kotlinx.coroutines.coroutineScope
class MoonpayService {
private val moonpayApi: MoonpayApi by lazy {
createRetrofitInstance(MoonpayApi.MOOONPAY_BASE_URL)
.create(MoonpayApi::class.java)
.create(MoonpayApi::class.java)
}
suspend fun getUserStatus(moonpayApiKey: String): Result<MoonPayUserStatus> {
return performRequest { moonpayApi.getUserStatus(moonpayApiKey) }
suspend fun getMoonpayStatus(moonpayApiKey: String): Result<MoonpayStatus> {
return try {
coroutineScope {
val userStatusResult = performRequest { moonpayApi.getUserStatus(moonpayApiKey) }
if (userStatusResult is Result.Failure) return@coroutineScope userStatusResult
val currenciesResult = performRequest { moonpayApi.getCurrencies(moonpayApiKey) }
if (currenciesResult is Result.Failure) return@coroutineScope currenciesResult
val userStatus = (userStatusResult as Result.Success).data
val currencies = (currenciesResult as Result.Success).data
val currenciesToBuy = mutableListOf<String>()
val currenciesToSell = mutableListOf<String>()
currencies.forEach { currencyStatus ->
if (currencyStatus.type != "crypto" || currencyStatus.isSuspended ||
!currencyStatus.supportsLiveMode
) {
return@forEach
}
if (userStatus.countryCode == "USA") {
if (!currencyStatus.isSupportedInUS) return@forEach
if (currencyStatus.notAllowedUSStates.contains(userStatus.stateCode)) return@forEach
}
val currencyCode = currencyStatus.code.uppercase()
currenciesToBuy.add(currencyCode)
if (currencyStatus.isSellSupported) currenciesToSell.add(currencyCode)
}
Result.Success(MoonpayStatus(
isBuyAllowed = userStatus.isBuyAllowed,
isSellAllowed = userStatus.isSellAllowed,
availableToBuy = if (userStatus.isBuyAllowed) currenciesToBuy else emptyList(),
availableToSell = if (userStatus.isSellAllowed) currenciesToSell else emptyList()
))
}
} catch (error: Error) {
Result.Failure(error)
}
}
}
data class MoonpayStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean,
val availableToBuy: List<String>,
val availableToSell: List<String>,
)
@JsonClass(generateAdapter = true)
data class MoonPayUserStatus(
val isBuyAllowed: Boolean,
val isSellAllowed: Boolean
val isSellAllowed: Boolean,
@Json(name = "isAllowed")
val isMoonpayAllowed: Boolean,
@Json(name = "alpha3")
val countryCode: String,
@Json(name = "state")
val stateCode: String,
)
@JsonClass(generateAdapter = true)
data class MoonpayCurrencies(
val type: String,
val code: String,
val supportsLiveMode: Boolean = false,
val isSuspended: Boolean = true,
val isSupportedInUS: Boolean = false,
val isSellSupported: Boolean = false,
val notAllowedUSStates: List<String> = emptyList(),
)

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_enabled="true" android:alpha="@dimen/material_emphasis_high_type" android:color="?attr/colorOnSurface"/>
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface"/>
</selector>

View file

@ -1,126 +1,88 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="249dp"
android:height="150dp"
android:viewportWidth="249"
android:viewportHeight="150">
<path
android:fillAlpha="0.03"
android:pathData="M10,0L238,0A9.999,10.001 134.998,0 1,248.001 10L248.017,140A9.999,10.001 134.998,0 1,238.019 150L10.019,150A9.999,10.001 134.998,0 1,0.017 140L0.001,10A9.999,10.001 134.998,0 1,10 0z" />
<path
android:fillAlpha="0.5"
android:pathData="M10,0L238,0A9.999,10.001 134.998,0 1,248.001 10L248.017,140A9.999,10.001 134.998,0 1,238.019 150L10.019,150A9.999,10.001 134.998,0 1,0.017 140L0.001,10A9.999,10.001 134.998,0 1,10 0z">
<aapt:attr name="android:fillColor">
<gradient
android:centerX="7.404067"
android:centerY="8.65384"
android:gradientRadius="267.36932"
android:type="radial">
<item
android:color="#FFFFFFFF"
android:offset="0" />
<item
android:color="#19FFFFFF"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
android:width="214dp"
android:height="130dp"
android:viewportWidth="214"
android:viewportHeight="130">
<path
android:fillColor="#1B1D1C"
android:pathData="M10,0L238,0A9.999,10.001 134.998,0 1,248.001 10L248.017,140A9.999,10.001 134.998,0 1,238.019 150L10.019,150A9.999,10.001 134.998,0 1,0.017 140L0.001,10A9.999,10.001 134.998,0 1,10 0z" />
android:pathData="M1.043,8.273C1.019,4.256 4.265,1 8.294,1H205.706C209.735,1 213.019,4.256 213.043,8.273L213.709,121.725C213.733,125.742 210.487,128.998 206.459,128.998H9.046C5.018,128.998 1.733,125.742 1.709,121.725L1.043,8.273Z" />
<path
android:fillColor="#00000000"
android:pathData="M10,0.5L238,0.5A9.499,9.501 134.998,0 1,247.501 10L247.517,140A9.499,9.501 134.998,0 1,238.019 149.5L10.019,149.5A9.499,9.501 134.998,0 1,0.517 140L0.501,10A9.499,9.501 134.998,0 1,10 0.5z"
android:strokeWidth="1">
<aapt:attr name="android:strokeColor">
<gradient
android:endX="246.96196"
android:endY="144.774"
android:startX="2.844399"
android:startY="3.38345"
android:type="linear">
<item
android:color="#FF1A1A1A"
android:offset="0" />
<item
android:color="#FF565656"
android:offset="1" />
</gradient>
</aapt:attr>
</path>
android:pathData="M1.043,8.273C1.019,4.256 4.265,1 8.294,1H205.706C209.735,1 213.019,4.256 213.043,8.273L213.709,121.725C213.733,125.742 210.487,128.998 206.459,128.998H9.046C5.018,128.998 1.733,125.742 1.709,121.725L1.043,8.273Z"
android:strokeWidth="0.5" />
<path
android:fillColor="#A5A5A5"
android:pathData="M28.359,121.942L26.389,120.734L26.472,123H25.41L25.498,120.734L23.505,121.95L23.002,121.003L25.044,119.992L23.009,118.974L23.511,118.058L25.497,119.25L25.415,117H26.468L26.389,119.258L28.365,118.058L28.884,118.974L26.826,120L28.885,121.011L28.359,121.942Z" />
android:pathData="M25.274,26.4L25.274,32.64L28.079,32.64C28.188,32.639 28.292,32.596 28.369,32.522C28.446,32.447 28.49,32.346 28.491,32.241L28.491,26.4L25.274,26.4Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M38.855,121.942L36.885,120.734L36.967,123H35.906L35.994,120.734L34.001,121.95L33.498,121.003L35.54,119.992L33.505,118.974L34.007,118.058L35.993,119.25L35.911,117H36.964L36.884,119.258L38.861,118.058L39.38,118.974L37.322,120L39.381,121.011L38.855,121.942Z" />
android:pathData="M19,32.241C19.001,32.346 19.045,32.447 19.122,32.522C19.199,32.596 19.303,32.639 19.412,32.64L22.217,32.64L22.217,26.4L19,26.4L19,32.241Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M49.351,121.942L47.381,120.734L47.463,123H46.402L46.49,120.734L44.497,121.95L43.994,121.003L46.035,119.992L44.001,118.974L44.503,118.058L46.489,119.25L46.407,117H47.46L47.38,119.258L49.357,118.058L49.876,118.974L47.818,120L49.877,121.011L49.351,121.942Z" />
android:pathData="M28.491,23.2L28.491,20.409C28.491,20.301 28.448,20.197 28.372,20.12C28.296,20.043 28.194,20 28.086,20L19.405,20C19.297,20 19.195,20.043 19.119,20.12C19.043,20.197 19,20.301 19,20.409L19,23.2L28.491,23.2Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M59.847,121.942L57.877,120.734L57.959,123H56.897L56.985,120.734L54.993,121.95L54.49,121.003L56.531,119.992L54.497,118.974L54.999,118.058L56.985,119.25L56.902,117H57.956L57.876,119.258L59.853,118.058L60.372,118.974L58.314,120L60.373,121.011L59.847,121.942Z" />
android:pathData="M43.503,24.163C42.922,23.921 42.292,23.812 41.661,23.846C40.827,23.844 39.996,23.934 39.183,24.113L39.183,25.532C39.832,25.455 40.483,25.412 41.136,25.4C41.486,25.385 41.836,25.418 42.177,25.5C42.286,25.532 42.385,25.59 42.465,25.669C42.546,25.748 42.605,25.846 42.637,25.953C42.74,26.334 42.783,26.728 42.766,27.122L42.766,27.539C41.997,27.498 41.385,27.476 40.965,27.476C40.519,27.454 40.073,27.539 39.667,27.725C39.347,27.903 39.109,28.195 38.999,28.54C38.847,29.044 38.779,29.569 38.796,30.094C38.74,30.771 38.901,31.448 39.257,32.029C39.445,32.243 39.683,32.409 39.95,32.513C40.216,32.618 40.505,32.659 40.791,32.632C41.127,32.632 42.172,32.605 42.633,32.007C42.709,31.912 42.774,31.808 42.826,31.698L42.877,32.496L44.572,32.496L44.572,27.222C44.596,26.539 44.52,25.857 44.346,25.196C44.284,24.977 44.178,24.772 44.033,24.594C43.888,24.417 43.708,24.27 43.503,24.163ZM42.748,29.256C42.796,29.736 42.713,30.219 42.508,30.656C42.409,30.782 42.272,30.876 42.117,30.924C41.942,30.972 41.76,30.995 41.578,30.992C41.294,31.028 41.006,30.953 40.777,30.783C40.623,30.523 40.558,30.221 40.592,29.922C40.582,29.681 40.607,29.44 40.666,29.206C40.684,29.139 40.716,29.075 40.761,29.021C40.805,28.966 40.861,28.921 40.924,28.889C41.098,28.821 41.285,28.791 41.472,28.803L42.748,28.803L42.748,29.256Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M76.235,121.942L74.266,120.734L74.348,123H73.286L73.374,120.734L71.381,121.95L70.879,121.003L72.92,119.992L70.885,118.974L71.387,118.058L73.373,119.25L73.291,117H74.345L74.265,119.258L76.242,118.058L76.761,118.974L74.703,120L76.762,121.011L76.235,121.942Z" />
android:pathData="M58.169,24.645C58.133,24.584 58.095,24.526 58.053,24.471C57.494,23.736 56.162,23.846 56.162,23.846C55.722,23.831 55.29,23.964 54.929,24.222C54.564,24.573 54.329,25.042 54.263,25.55C54.001,27.328 54.001,29.136 54.263,30.914C54.326,31.417 54.555,31.882 54.911,32.233C55.299,32.52 55.774,32.659 56.251,32.623C56.251,32.623 56.278,32.623 56.296,32.623C56.296,32.623 57.498,32.72 58.084,32.104C58.095,32.607 58.062,33.11 57.985,33.606C57.966,33.738 57.915,33.864 57.837,33.971C57.759,34.078 57.656,34.164 57.539,34.222C57.166,34.351 56.773,34.406 56.381,34.382L54.535,34.35L54.535,35.788C54.876,35.853 55.219,35.9 55.563,35.93C55.988,35.972 56.39,35.99 56.774,35.99C57.49,36.044 58.205,35.884 58.834,35.53C59.055,35.374 59.242,35.174 59.386,34.943C59.529,34.71 59.626,34.451 59.67,34.18C59.813,33.297 59.875,32.402 59.853,31.507L59.853,23.979L58.209,23.979L58.169,24.645ZM57.963,30.221C57.936,30.452 57.824,30.664 57.65,30.813C57.447,30.94 57.212,30.999 56.975,30.983C56.711,31.006 56.446,30.95 56.211,30.823C56.028,30.636 55.923,30.385 55.916,30.12C55.815,28.876 55.815,27.625 55.916,26.381C55.923,26.125 56.025,25.882 56.202,25.701C56.442,25.579 56.709,25.526 56.975,25.55C57.227,25.529 57.479,25.599 57.686,25.747C57.87,25.947 57.981,26.207 57.999,26.482C58.066,27.072 58.093,27.666 58.079,28.26C58.101,28.916 58.062,29.573 57.963,30.221Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M86.731,121.942L84.761,120.734L84.844,123H83.782L83.87,120.734L81.877,121.95L81.374,121.003L83.416,119.992L81.381,118.974L81.883,118.058L83.869,119.25L83.787,117H84.841L84.761,119.258L86.737,118.058L87.257,118.974L85.199,120L87.258,121.011L86.731,121.942Z" />
android:pathData="M66.713,24.124C66.131,23.787 65.461,23.636 64.793,23.691C64.098,23.642 63.404,23.802 62.799,24.152C62.358,24.473 62.062,24.959 61.975,25.502C61.831,26.388 61.77,27.286 61.793,28.184C61.769,29.083 61.833,29.983 61.984,30.87C62.031,31.139 62.131,31.395 62.278,31.623C62.425,31.851 62.616,32.046 62.84,32.197C63.478,32.531 64.194,32.681 64.911,32.631C65.367,32.63 65.823,32.603 66.276,32.552C66.653,32.517 67.025,32.453 67.392,32.359L67.392,30.916C66.554,31.004 65.867,31.05 65.33,31.05C64.933,31.074 64.535,31.025 64.155,30.907C64.038,30.857 63.936,30.778 63.856,30.677C63.777,30.577 63.723,30.458 63.7,30.331C63.618,29.823 63.585,29.307 63.6,28.792L67.574,28.792L67.574,28.165C67.596,27.241 67.548,26.317 67.433,25.4C67.368,24.896 67.109,24.438 66.713,24.124ZM65.849,27.428L63.6,27.428C63.6,26.95 63.632,26.473 63.696,26C63.704,25.887 63.736,25.777 63.79,25.678C63.843,25.579 63.917,25.492 64.005,25.424C64.473,25.233 64.994,25.233 65.462,25.424C65.548,25.494 65.619,25.583 65.671,25.683C65.723,25.782 65.754,25.892 65.762,26.004C65.833,26.475 65.862,26.952 65.849,27.428Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M97.227,121.942L95.257,120.734L95.34,123H94.278L94.366,120.734L92.373,121.95L91.87,121.003L93.912,119.992L91.877,118.974L92.379,118.058L94.365,119.25L94.283,117H95.337L95.257,119.258L97.233,118.058L97.752,118.974L95.695,120L97.753,121.011L97.227,121.942Z" />
android:pathData="M78.824,25.12C78.747,24.744 78.542,24.406 78.243,24.161C77.886,23.928 77.46,23.817 77.032,23.844C77.032,23.844 76.991,23.844 76.963,23.844C76.963,23.844 75.581,23.767 75.01,24.464C74.948,24.542 74.892,24.625 74.844,24.712C74.752,24.478 74.592,24.275 74.383,24.129C74.024,23.923 73.61,23.826 73.195,23.848C73.195,23.848 71.813,23.767 71.242,24.468C71.178,24.545 71.122,24.629 71.076,24.717L71.03,23.984L69.349,23.984L69.349,32.48L71.191,32.48L71.191,28.051C71.182,27.52 71.201,26.988 71.251,26.459C71.263,26.198 71.362,25.949 71.532,25.749C71.736,25.584 71.997,25.506 72.26,25.531C72.493,25.513 72.727,25.557 72.937,25.658C73.015,25.731 73.076,25.819 73.119,25.916C73.162,26.013 73.185,26.118 73.186,26.223C73.234,26.747 73.254,27.272 73.245,27.798L73.245,32.48L75.088,32.48L75.088,28.051C75.088,27.314 75.088,26.785 75.143,26.459C75.156,26.197 75.258,25.946 75.434,25.749C75.642,25.586 75.906,25.508 76.171,25.531C76.405,25.513 76.641,25.557 76.852,25.658C76.929,25.732 76.99,25.82 77.032,25.917C77.074,26.014 77.096,26.118 77.096,26.223C77.147,26.747 77.167,27.272 77.156,27.798L77.156,32.48L78.999,32.48L78.999,26.884C79.009,26.291 78.951,25.699 78.824,25.12Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M107.723,121.942L105.753,120.734L105.835,123H104.774L104.862,120.734L102.869,121.95L102.366,121.003L104.408,119.992L102.373,118.974L102.875,118.058L104.861,119.25L104.779,117H105.832L105.752,119.258L107.729,118.058L108.248,118.974L106.19,120L108.249,121.011L107.723,121.942Z" />
android:pathData="M51.691,24.171C51.307,23.928 50.851,23.814 50.394,23.845C50.394,23.845 50.356,23.845 50.338,23.845C50.338,23.845 48.938,23.773 48.364,24.465C48.323,24.514 48.286,24.565 48.252,24.619L48.21,23.981L46.507,23.981L46.507,32.48L48.373,32.48L48.373,28.049C48.363,27.521 48.384,26.992 48.434,26.465C48.448,26.203 48.553,25.952 48.733,25.755C48.958,25.584 49.242,25.503 49.526,25.528C49.774,25.511 50.023,25.555 50.249,25.655C50.333,25.728 50.401,25.816 50.448,25.915C50.496,26.014 50.521,26.121 50.524,26.23C50.578,26.75 50.6,27.273 50.59,27.796L50.59,32.48L52.456,32.48L52.456,26.963C52.471,26.356 52.419,25.749 52.302,25.153C52.226,24.765 52.009,24.417 51.691,24.171Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M124.112,121.942L122.142,120.734L122.224,123H121.163L121.251,120.734L119.258,121.95L118.755,121.003L120.796,119.992L118.762,118.974L119.264,118.058L121.25,119.25L121.168,117H122.221L122.141,119.258L124.118,118.058L124.637,118.974L122.579,120L124.638,121.011L124.112,121.942Z" />
android:pathData="M36.543,21.44L34.653,21.44L34.653,23.877L33.477,23.877L33.477,25.434L34.653,25.434L34.653,32.48L36.543,32.48L36.543,25.434L37.981,25.434L37.981,23.877L36.543,23.877L36.543,21.44Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M134.608,121.942L132.638,120.734L132.72,123H131.659L131.746,120.734L129.754,121.95L129.251,121.003L131.292,119.992L129.258,118.974L129.76,118.058L131.746,119.25L131.664,117H132.717L132.637,119.258L134.614,118.058L135.133,118.974L133.075,120L135.134,121.011L134.608,121.942Z" />
android:pathData="M23.463,105.118L21.826,104.112L21.904,106H21.024L21.087,104.112L19.441,105.125L19.02,104.335L20.707,103.493L19.016,102.645L19.428,101.882L21.08,102.875L21.001,101H21.874L21.818,102.882L23.451,101.882L23.885,102.645L22.184,103.5L23.895,104.342L23.463,105.118Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M145.104,121.942L143.134,120.734L143.216,123H142.154L142.242,120.734L140.249,121.95L139.747,121.003L141.788,119.992L139.754,118.974L140.256,118.058L142.242,119.25L142.159,117H143.213L143.133,119.258L145.11,118.058L145.629,118.974L143.571,120L145.63,121.011L145.104,121.942Z" />
android:pathData="M32.161,105.118L30.524,104.112L30.602,106H29.722L29.785,104.112L28.139,105.125L27.718,104.335L29.405,103.493L27.715,102.645L28.126,101.882L29.778,102.875L29.699,101H30.573L30.517,102.882L32.149,101.882L32.583,102.645L30.883,103.5L32.593,104.342L32.161,105.118Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M155.599,121.942L153.63,120.734L153.712,123H152.65L152.738,120.734L150.745,121.95L150.242,121.003L152.284,119.992L150.249,118.974L150.751,118.058L152.737,119.25L152.655,117H153.709L153.629,119.258L155.606,118.058L156.125,118.974L154.067,120L156.126,121.011L155.599,121.942Z" />
android:pathData="M40.86,105.118L39.222,104.112L39.3,106H38.42L38.483,104.112L36.837,105.125L36.416,104.335L38.104,103.493L36.413,102.645L36.825,101.882L38.476,102.875L38.398,101H39.271L39.215,102.882L40.847,101.882L41.282,102.645L39.581,103.5L41.292,104.342L40.86,105.118Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M171.988,121.942L170.018,120.734L170.101,123H169.039L169.127,120.734L167.134,121.95L166.631,121.003L168.673,119.992L166.638,118.974L167.14,118.058L169.126,119.25L169.044,117H170.098L170.018,119.258L171.994,118.058L172.513,118.974L170.456,120L172.515,121.011L171.988,121.942Z" />
android:pathData="M49.558,105.118L47.92,104.112L47.998,106H47.119L47.181,104.112L45.535,105.125L45.114,104.335L46.802,103.493L45.111,102.645L45.523,101.882L47.174,102.875L47.096,101H47.969L47.913,102.882L49.546,101.882L49.98,102.645L48.279,103.5L49.99,104.342L49.558,105.118Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M182.484,121.942L180.514,120.734L180.597,123H179.535L179.623,120.734L177.63,121.95L177.127,121.003L179.169,119.992L177.134,118.974L177.636,118.058L179.622,119.25L179.54,117H180.593L180.514,119.258L182.49,118.058L183.009,118.974L180.952,120L183.01,121.011L182.484,121.942Z" />
android:pathData="M63.14,105.118L61.502,104.112L61.58,106H60.701L60.763,104.112L59.117,105.125L58.696,104.335L60.384,103.493L58.693,102.645L59.105,101.882L60.756,102.875L60.678,101H61.551L61.495,102.882L63.128,101.882L63.562,102.645L61.861,103.5L63.572,104.342L63.14,105.118Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M192.98,121.942L191.01,120.734L191.092,123H190.031L190.119,120.734L188.126,121.95L187.623,121.003L189.665,119.992L187.63,118.974L188.132,118.058L190.118,119.25L190.036,117H191.089L191.009,119.258L192.986,118.058L193.505,118.974L191.447,120L193.506,121.011L192.98,121.942Z" />
android:pathData="M71.838,105.118L70.2,104.112L70.279,106H69.399L69.462,104.112L67.815,105.125L67.394,104.335L69.082,103.493L67.391,102.645L67.803,101.882L69.454,102.875L69.376,101H70.249L70.193,102.882L71.826,101.882L72.26,102.645L70.559,103.5L72.27,104.342L71.838,105.118Z" />
<path
android:fillColor="#A5A5A5"
android:pathData="M203.476,121.942L201.506,120.734L201.588,123H200.527L200.615,120.734L198.622,121.95L198.119,121.003L200.16,119.992L198.126,118.974L198.628,118.058L200.614,119.25L200.532,117H201.585L201.505,119.258L203.482,118.058L204.001,118.974L201.943,120L204.002,121.011L203.476,121.942Z" />
android:pathData="M80.536,105.118L78.898,104.112L78.977,106H78.097L78.16,104.112L76.514,105.125L76.093,104.335L77.78,103.493L76.089,102.645L76.501,101.882L78.152,102.875L78.074,101H78.947L78.891,102.882L80.524,101.882L80.958,102.645L79.257,103.5L80.968,104.342L80.536,105.118Z" />
<path
android:fillColor="#ffffff"
android:pathData="M26.483,30.4L26.483,36.64L29.382,36.64C29.494,36.639 29.602,36.596 29.681,36.522C29.761,36.447 29.806,36.346 29.807,36.241L29.807,30.4L26.483,30.4Z" />
android:fillColor="#A5A5A5"
android:pathData="M89.234,105.118L87.597,104.112L87.675,106H86.795L86.858,104.112L85.212,105.125L84.791,104.335L86.478,103.493L84.788,102.645L85.2,101.882L86.851,102.875L86.772,101H87.646L87.59,102.882L89.222,101.882L89.657,102.645L87.956,103.5L89.666,104.342L89.234,105.118Z" />
<path
android:fillColor="#ffffff"
android:pathData="M20,36.241C20.001,36.346 20.046,36.447 20.126,36.522C20.205,36.596 20.313,36.639 20.425,36.64L23.324,36.64L23.324,30.4L20,30.4L20,36.241Z" />
android:fillColor="#A5A5A5"
android:pathData="M102.816,105.118L101.179,104.112L101.257,106H100.377L100.44,104.112L98.794,105.125L98.373,104.335L100.06,103.493L98.369,102.645L98.781,101.882L100.433,102.875L100.354,101H101.227L101.171,102.882L102.804,101.882L103.238,102.645L101.538,103.5L103.248,104.342L102.816,105.118Z" />
<path
android:fillColor="#ffffff"
android:pathData="M29.807,27.2L29.807,24.409C29.807,24.301 29.763,24.197 29.685,24.12C29.606,24.043 29.5,24 29.389,24L20.418,24C20.307,24 20.201,24.043 20.123,24.12C20.044,24.197 20,24.301 20,24.409L20,27.2L29.807,27.2Z" />
android:fillColor="#A5A5A5"
android:pathData="M111.515,105.118L109.877,104.112L109.955,106H109.075L109.138,104.112L107.492,105.125L107.071,104.335L108.758,103.493L107.068,102.645L107.48,101.882L109.131,102.875L109.053,101H109.926L109.87,102.882L111.502,101.882L111.937,102.645L110.236,103.5L111.947,104.342L111.515,105.118Z" />
<path
android:fillColor="#ffffff"
android:pathData="M45.32,28.163C44.719,27.921 44.068,27.812 43.416,27.846C42.555,27.844 41.696,27.934 40.856,28.113L40.856,29.532C41.526,29.455 42.199,29.412 42.874,29.4C43.236,29.385 43.598,29.418 43.949,29.5C44.062,29.532 44.164,29.59 44.247,29.669C44.33,29.748 44.392,29.846 44.425,29.953C44.531,30.334 44.576,30.728 44.558,31.122L44.558,31.539C43.764,31.498 43.131,31.476 42.698,31.476C42.236,31.454 41.775,31.539 41.356,31.725C41.025,31.903 40.779,32.195 40.666,32.54C40.509,33.044 40.438,33.569 40.456,34.095C40.398,34.771 40.564,35.448 40.932,36.029C41.127,36.243 41.372,36.409 41.648,36.513C41.924,36.618 42.222,36.659 42.517,36.632C42.864,36.632 43.945,36.605 44.421,36.007C44.499,35.912 44.566,35.808 44.62,35.699L44.673,36.496L46.424,36.496L46.424,31.222C46.45,30.539 46.371,29.857 46.191,29.196C46.127,28.977 46.017,28.772 45.867,28.594C45.717,28.417 45.531,28.27 45.32,28.163ZM44.54,33.256C44.589,33.736 44.503,34.219 44.292,34.656C44.189,34.783 44.048,34.876 43.887,34.924C43.706,34.972 43.519,34.995 43.331,34.992C43.037,35.028 42.74,34.953 42.503,34.783C42.343,34.523 42.277,34.221 42.312,33.922C42.302,33.681 42.327,33.44 42.388,33.206C42.407,33.139 42.44,33.076 42.486,33.021C42.532,32.966 42.59,32.921 42.655,32.889C42.834,32.821 43.028,32.791 43.221,32.803L44.54,32.803L44.54,33.256Z" />
android:fillColor="#A5A5A5"
android:pathData="M120.213,105.118L118.575,104.112L118.653,106H117.774L117.836,104.112L116.19,105.125L115.769,104.335L117.457,103.493L115.766,102.645L116.178,101.882L117.829,102.875L117.751,101H118.624L118.568,102.882L120.201,101.882L120.635,102.645L118.934,103.5L120.645,104.342L120.213,105.118Z" />
<path
android:fillColor="#ffffff"
android:pathData="M60.474,28.645C60.438,28.584 60.398,28.526 60.354,28.471C59.777,27.736 58.401,27.846 58.401,27.846C57.946,27.832 57.499,27.964 57.126,28.222C56.75,28.573 56.507,29.042 56.438,29.55C56.168,31.328 56.168,33.136 56.438,34.915C56.504,35.417 56.74,35.882 57.108,36.233C57.509,36.52 58,36.659 58.493,36.623C58.493,36.623 58.521,36.623 58.539,36.623C58.539,36.623 59.782,36.72 60.386,36.104C60.398,36.607 60.364,37.11 60.285,37.606C60.264,37.738 60.212,37.864 60.131,37.971C60.051,38.078 59.945,38.164 59.823,38.222C59.439,38.351 59.033,38.406 58.627,38.382L56.72,38.35L56.72,39.788C57.071,39.853 57.426,39.9 57.782,39.93C58.221,39.972 58.636,39.99 59.034,39.99C59.773,40.044 60.512,39.884 61.162,39.53C61.39,39.374 61.583,39.174 61.732,38.943C61.88,38.71 61.98,38.451 62.026,38.18C62.174,37.297 62.237,36.402 62.215,35.507L62.215,27.979L60.516,27.979L60.474,28.645ZM60.262,34.221C60.233,34.452 60.118,34.664 59.938,34.813C59.729,34.939 59.486,34.999 59.241,34.984C58.968,35.006 58.694,34.95 58.452,34.823C58.263,34.636 58.153,34.384 58.147,34.12C58.042,32.876 58.042,31.625 58.147,30.381C58.153,30.125 58.259,29.882 58.442,29.701C58.69,29.579 58.966,29.526 59.241,29.55C59.502,29.529 59.761,29.599 59.975,29.747C60.166,29.947 60.28,30.207 60.299,30.482C60.369,31.072 60.396,31.666 60.382,32.26C60.405,32.916 60.364,33.573 60.262,34.221Z" />
android:fillColor="#A5A5A5"
android:pathData="M128.911,105.118L127.273,104.112L127.352,106H126.472L126.535,104.112L124.888,105.125L124.467,104.335L126.155,103.493L124.464,102.645L124.876,101.882L126.527,102.875L126.449,101H127.322L127.266,102.882L128.899,101.882L129.333,102.645L127.632,103.5L129.343,104.342L128.911,105.118Z" />
<path
android:fillColor="#ffffff"
android:pathData="M69.304,28.124C68.702,27.787 68.01,27.636 67.319,27.691C66.601,27.642 65.884,27.802 65.259,28.152C64.804,28.473 64.497,28.959 64.408,29.502C64.259,30.388 64.196,31.286 64.219,32.184C64.195,33.083 64.261,33.983 64.417,34.87C64.465,35.139 64.568,35.395 64.72,35.623C64.872,35.851 65.07,36.046 65.301,36.197C65.961,36.531 66.701,36.681 67.441,36.631C67.913,36.63 68.384,36.603 68.852,36.552C69.241,36.517 69.626,36.453 70.005,36.359L70.005,34.916C69.139,35.004 68.429,35.05 67.874,35.05C67.464,35.074 67.053,35.025 66.66,34.907C66.54,34.857 66.433,34.778 66.351,34.677C66.269,34.577 66.214,34.458 66.19,34.331C66.106,33.822 66.071,33.307 66.087,32.792L70.193,32.792L70.193,32.165C70.215,31.241 70.167,30.317 70.047,29.4C69.98,28.896 69.713,28.438 69.304,28.124ZM68.41,31.428L66.087,31.428C66.086,30.95 66.119,30.473 66.186,30C66.195,29.887 66.228,29.777 66.283,29.678C66.338,29.579 66.414,29.492 66.505,29.424C66.988,29.233 67.527,29.233 68.01,29.424C68.1,29.494 68.173,29.583 68.227,29.682C68.28,29.782 68.312,29.892 68.321,30.004C68.394,30.475 68.424,30.952 68.41,31.428Z" />
android:fillColor="#A5A5A5"
android:pathData="M142.493,105.118L140.855,104.112L140.933,106H140.054L140.116,104.112L138.47,105.125L138.049,104.335L139.737,103.493L138.046,102.645L138.458,101.882L140.109,102.875L140.031,101H140.904L140.848,102.882L142.481,101.882L142.915,102.645L141.214,103.5L142.925,104.342L142.493,105.118Z" />
<path
android:fillColor="#ffffff"
android:pathData="M81.818,29.12C81.739,28.745 81.527,28.406 81.218,28.161C80.849,27.928 80.409,27.817 79.966,27.844C79.966,27.844 79.924,27.844 79.895,27.844C79.895,27.844 78.467,27.767 77.877,28.464C77.813,28.542 77.755,28.625 77.705,28.713C77.61,28.478 77.445,28.275 77.229,28.129C76.858,27.924 76.431,27.826 76.001,27.848C76.001,27.848 74.573,27.767 73.983,28.468C73.917,28.545 73.86,28.629 73.812,28.717L73.764,27.984L72.027,27.984L72.027,36.48L73.931,36.48L73.931,32.051C73.921,31.52 73.942,30.988 73.993,30.459C74.005,30.198 74.107,29.949 74.283,29.749C74.493,29.584 74.764,29.506 75.035,29.531C75.276,29.513 75.518,29.557 75.735,29.658C75.815,29.731 75.879,29.819 75.923,29.916C75.967,30.013 75.991,30.118 75.992,30.224C76.042,30.747 76.063,31.272 76.054,31.798L76.054,36.48L77.958,36.48L77.958,32.051C77.958,31.314 77.958,30.785 78.015,30.459C78.028,30.197 78.133,29.946 78.315,29.749C78.53,29.586 78.802,29.508 79.076,29.531C79.319,29.513 79.562,29.557 79.781,29.658C79.86,29.732 79.923,29.82 79.966,29.917C80.01,30.014 80.033,30.118 80.033,30.224C80.086,30.747 80.106,31.272 80.095,31.798L80.095,36.48L81.999,36.48L81.999,30.884C82.009,30.291 81.949,29.699 81.818,29.12Z" />
android:fillColor="#A5A5A5"
android:pathData="M151.191,105.118L149.553,104.112L149.632,106H148.752L148.815,104.112L147.169,105.125L146.748,104.335L148.435,103.493L146.744,102.645L147.156,101.882L148.807,102.875L148.729,101H149.602L149.546,102.882L151.179,101.882L151.613,102.645L149.912,103.5L151.623,104.342L151.191,105.118Z" />
<path
android:fillColor="#ffffff"
android:pathData="M53.781,28.171C53.383,27.928 52.913,27.814 52.44,27.845C52.44,27.845 52.402,27.845 52.382,27.845C52.382,27.845 50.936,27.773 50.343,28.465C50.301,28.514 50.262,28.565 50.227,28.619L50.184,27.981L48.424,27.981L48.424,36.48L50.353,36.48L50.353,32.049C50.342,31.521 50.363,30.992 50.415,30.465C50.429,30.203 50.538,29.952 50.724,29.755C50.957,29.584 51.25,29.503 51.543,29.529C51.8,29.511 52.057,29.555 52.291,29.655C52.378,29.728 52.448,29.816 52.497,29.915C52.546,30.014 52.572,30.121 52.575,30.23C52.63,30.75 52.653,31.273 52.643,31.796L52.643,36.48L54.571,36.48L54.571,30.963C54.587,30.356 54.533,29.75 54.412,29.153C54.333,28.765 54.109,28.417 53.781,28.171Z" />
android:fillColor="#A5A5A5"
android:pathData="M159.889,105.118L158.252,104.112L158.33,106H157.45L157.513,104.112L155.867,105.125L155.446,104.335L157.133,103.493L155.443,102.645L155.854,101.882L157.506,102.875L157.427,101H158.3L158.244,102.882L159.877,101.882L160.311,102.645L158.611,103.5L160.321,104.342L159.889,105.118Z" />
<path
android:fillColor="#ffffff"
android:pathData="M38.128,25.44L36.175,25.44L36.175,27.877L34.96,27.877L34.96,29.434L36.175,29.434L36.175,36.48L38.128,36.48L38.128,29.434L39.614,29.434L39.614,27.877L38.128,27.877L38.128,25.44Z" />
android:fillColor="#A5A5A5"
android:pathData="M168.588,105.118L166.95,104.112L167.028,106H166.148L166.211,104.112L164.565,105.125L164.144,104.335L165.831,103.493L164.141,102.645L164.553,101.882L166.204,102.875L166.126,101H166.999L166.943,102.882L168.575,101.882L169.01,102.645L167.309,103.5L169.02,104.342L168.588,105.118Z" />
</vector>

View file

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:tint="#1C1C1E" android:viewportHeight="24"
android:viewportWidth="24" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z"/>
</vector>

View file

@ -0,0 +1,11 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:autoMirrored="true"
android:tint="#1C1C1E"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M8.59,16.59L13.17,12 8.59,7.41 10,6l6,6 -6,6 -1.41,-1.41z" />
</vector>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="20dp"
android:viewportWidth="24"
android:viewportHeight="20">
<group>
<path
android:pathData="M5.853,11.6645L9.7814,8.075C10.0495,7.8439 10.0734,7.3871 9.84,7.1191C9.6067,6.8511 9.1615,6.824 8.9084,7.0724L6.0712,9.6665L6.0712,0.8398C6.0712,0.468 5.7781,0.1667 5.4165,0.1667C5.0549,0.1667 4.7618,0.468 4.7618,0.8398L4.7618,9.6665L1.9246,7.0724C1.6715,6.824 1.2271,6.8576 0.9937,7.1256C0.7604,7.3936 0.7835,7.8439 1.0516,8.075L4.98,11.6645C5.2976,11.8989 5.5827,11.8802 5.853,11.6645L5.853,11.6645Z"
android:fillColor="#ffffff"/>
</group>
</vector>

View file

@ -24,24 +24,26 @@
app:layout_constraintTop_toBottomOf="@+id/pseudo_toolbar" />
<TextView
android:id="@+id/tv_scan_address"
android:id="@+id/tv_recieve_message"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="@string/onboarding_dialog_wallet_address"
android:layout_marginStart="40dp"
android:layout_marginEnd="40dp"
android:textAlignment="center"
android:textAllCaps="false"
app:layout_constraintEnd_toEndOf="@+id/imv_qr_code"
app:layout_constraintStart_toStartOf="@+id/imv_qr_code"
app:layout_constraintTop_toBottomOf="@+id/imv_qr_code" />
app:layout_constraintBottom_toTopOf="@+id/guideline3"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imv_qr_code"
tools:text="@string/address_qr_code_message_token_format" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.8047809" />
app:layout_constraintGuide_percent="0.8" />
<FrameLayout
android:id="@+id/btn_fl_copy_address"
@ -53,7 +55,7 @@
android:backgroundTint="@color/lightGray0"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:layout_constraintEnd_toStartOf="@+id/btn_fl_explore"
app:layout_constraintEnd_toStartOf="@+id/btn_fl_share"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline3">
@ -83,6 +85,7 @@
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_copy_green" />
</androidx.appcompat.widget.LinearLayoutCompat>
@ -90,7 +93,7 @@
<FrameLayout
android:id="@+id/btn_fl_explore"
android:id="@+id/btn_fl_share"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:layout_marginEnd="32dp"
@ -104,11 +107,12 @@
app:layout_constraintTop_toTopOf="@+id/guideline3">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_gravity="center"
android:background="?selectableItemBackgroundBorderless"
android:src="@drawable/ic_share_green" />
android:src="@drawable/ic_share_chip"
android:tint="@color/accent" />
</FrameLayout>

View file

@ -22,7 +22,7 @@
android:layout_height="?attr/actionBarSize"
app:menu="@menu/popular_tokens"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
app:title="Add Currencies" />
app:title="Add tokens" />
</com.google.android.material.appbar.AppBarLayout>

View file

@ -43,27 +43,46 @@
android:layout_marginEnd="16dp"
android:layout_marginBottom="33dp">
<TextView
android:id="@+id/tv_card_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingBottom="4dp"
android:text="@string/details_section_title_card"
android:textAllCaps="true"
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<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_card_id_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_cid"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/tv_card_title" />
<TextView
android:id="@+id/tv_card_id"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_card_title"
tools:text="0000 0000 0000 0000" />
<TextView
@ -115,33 +134,47 @@
tools:text="48 hashes" />
<TextView
android:id="@+id/tv_disclaimer"
android:id="@+id/tv_security"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title"
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_row_title_manage_security"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes" />
<TextView
android:id="@+id/tv_erase_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/disclaimer_title"
android:text="@string/details_row_title_erase_wallet"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_signed_hashes_title" />
<TextView
android:id="@+id/tv_card_tou"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="10dp"
android:text="@string/details_row_title_card_tou"
android:textColor="@color/darkGray6"
android:textSize="16sp"
android:visibility="gone"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_disclaimer" />
app:layout_constraintTop_toBottomOf="@id/tv_security_title" />
<TextView
android:id="@+id/tv_settings_title"
@ -149,12 +182,12 @@
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:paddingBottom="4dp"
android:text="@string/details_section_title_settings"
android:text="@string/details_section_title_app"
android:textAllCaps="true"
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_card_tou" />
app:layout_constraintTop_toBottomOf="@id/tv_erase_wallet" />
<View
android:layout_width="match_parent"
@ -162,6 +195,7 @@
android:background="@color/separatorGrey2"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title" />
<TextView
android:id="@+id/tv_app_currency"
android:layout_width="wrap_content"
@ -171,6 +205,7 @@
android:textColor="@color/darkGray1"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title"
tools:text="USD" />
@ -187,6 +222,37 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_settings_title" />
<TextView
android:id="@+id/tv_disclaimer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/disclaimer_title"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
<TextView
android:id="@+id/tv_card_tou"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="10dp"
android:text="@string/details_row_title_card_tou"
android:textColor="@color/darkGray6"
android:textSize="16sp"
android:visibility="gone"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_disclaimer" />
<TextView
android:id="@+id/tv_send_feedback"
android:layout_width="match_parent"
@ -197,8 +263,9 @@
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_app_currency_title" />
app:layout_constraintTop_toBottomOf="@id/tv_card_tou" />
<TextView
android:id="@+id/tv_wallet_connect"
@ -210,76 +277,10 @@
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:drawableEndCompat="@drawable/ic_angle_bracket_right"
app:drawableTint="@color/darkGray1"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_send_feedback" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="tv_wallet_connect,tv_send_feedback" />
<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_section_title_card"
android:textAllCaps="true"
android:textColor="@color/colorSecondary"
android:textSize="13sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/barrier" />
<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_security"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
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_card_title"
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_row_title_manage_security"
android:textColor="@color/darkGray6"
android:textSize="16sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_card_title" />
<TextView
android:id="@+id/tv_erase_wallet"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="15dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:text="@string/details_row_title_erase_wallet"
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" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -14,6 +14,7 @@
style="@style/Widget.MaterialComponents.Toolbar.Surface"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
android:fitsSystemWindows="false"
app:liftOnScroll="true">

View file

@ -23,6 +23,7 @@
android:layout_height="?attr/actionBarSize"
app:menu="@menu/wallet_details"
app:navigationIcon="@drawable/ic_baseline_arrow_back_24"
android:theme="@style/ThemeOverlay.MyTheme.Toolbar"
app:title=" " />
</com.google.android.material.appbar.AppBarLayout>
@ -131,7 +132,7 @@
app:constraint_referenced_ids="l_wallet_details,card_pending_transaction_warning" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_top_up"
android:id="@+id/btn_trade"
style="@style/TapButtonWithIcon"
android:layout_width="0dp"
android:layout_marginStart="16dp"
@ -151,7 +152,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="btn_top_up, btn_sell" />
app:constraint_referenced_ids="btn_trade, btn_sell" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_right_button"
@ -172,7 +173,7 @@
app:icon="@drawable/ic_arrow_down"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@id/btn_top_up"
app:layout_constraintStart_toEndOf="@id/btn_trade"
app:layout_constraintEnd_toStartOf="@id/btn_confirm"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintVertical_bias="1" />

View file

@ -2,6 +2,7 @@
<androidx.constraintlayout.widget.ConstraintLayout 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/cl_subtitle_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="10dp"
@ -20,4 +21,19 @@
app:layout_constraintTop_toTopOf="parent"
tools:text="BLOCKCHAINS" />
<ImageView
android:id="@+id/iv_toggle_sublist_visibility"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:textAllCaps="true"
android:textColor="@color/darkGray1"
android:textSize="13sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_subtitle"
app:layout_constraintBottom_toBottomOf="@id/tv_subtitle"
tools:src="@drawable/ic_arrow_angle_right" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -52,33 +52,56 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5"/>
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/tv_currency"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:paddingStart="16dp"
android:paddingEnd="2dp"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintStart_toEndOf="@id/iv_currency"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="@id/vertical_guideline"
app:layout_constraintBottom_toBottomOf="@id/guideline"
tools:text="Bitcoin" />
app:layout_constraintEnd_toStartOf="@+id/tv_amount"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="parent"
tools:text="Binance Smart Chain Optimal" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/vertical_guideline"
android:id="@+id/guideline2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintGuide_percent="0.5" />
app:layout_constraintGuide_percent="0.45"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_amount"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="4dp"
android:ellipsize="end"
android:gravity="end"
android:maxEms="12"
android:maxLength="12"
android:maxLines="1"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
android:visibility="visible"
app:layout_constraintEnd_toStartOf="@+id/tv_currency_symbol"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toStartOf="@+id/guideline2"
app:layout_constraintTop_toTopOf="parent"
tools:text="1234567890.1234567890" />
<TextView
android:id="@+id/tv_exchange_rate"
@ -90,8 +113,8 @@
android:textColor="@color/darkGray2"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/guideline"
tools:text="USD 3 588" />
<TextView
@ -105,8 +128,8 @@
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline"
app:layout_constraintStart_toEndOf="@id/iv_currency" />
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/guideline" />
<TextView
android:id="@+id/tv_status_loading"
@ -123,29 +146,10 @@
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<TextView
android:id="@+id/tv_amount"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginTop="16dp"
android:ellipsize="end"
android:gravity="end"
android:maxLines="1"
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@id/tv_currency_symbol"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@id/vertical_guideline"
app:layout_constraintTop_toTopOf="parent"
tools:text="3 588" />
<TextView
android:id="@+id/tv_currency_symbol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:gravity="end"
@ -155,7 +159,6 @@
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_weight="1"
app:layout_constraintStart_toEndOf="@id/tv_amount"
app:layout_constraintTop_toTopOf="parent"
tools:text="BTC" />
@ -168,8 +171,8 @@
android:textColor="@color/darkGray2"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@id/tv_exchange_rate"
app:layout_constraintTop_toTopOf="@id/tv_exchange_rate"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/tv_exchange_rate"
tools:text="0.43 USD" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -4,9 +4,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="52dp"
android:layout_marginBottom="1dp"
android:background="@android:color/white">
android:background="@android:color/white"
android:minHeight="60dp">
<ImageView
android:id="@+id/iv_currency"
@ -14,6 +14,7 @@
android:layout_height="40dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:contentDescription="@string/token_icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
@ -37,7 +38,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.5"/>
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/tv_currency_name"
@ -50,27 +51,27 @@
android:textColor="@color/darkGray6"
android:textSize="17sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintEnd_toStartOf="@id/barrier_chip_button"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintVertical_bias="1"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="@id/guideline"
app:layout_constraintVertical_bias="1"
tools:text="NODLE" />
<TextView
android:id="@+id/tv_currency_symbol"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintVertical_bias="0"
android:paddingStart="16dp"
android:paddingEnd="16dp"
android:textColor="@color/darkGray6"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="@id/guideline"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintStart_toEndOf="@id/iv_currency"
app:layout_constraintTop_toTopOf="@id/guideline"
app:layout_constraintVertical_bias="0"
tools:text="NODLE" />
<com.google.android.material.chip.Chip
@ -90,11 +91,11 @@
<com.google.android.material.chip.Chip
android:id="@+id/btn_token_added"
android:enabled="false"
style="@style/Widget.MaterialComponents.Chip.Action"
android:layout_width="102dp"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:enabled="false"
android:src="@drawable/ic_inactive"
android:text="@string/add_token_added"
android:textAlignment="center"

View file

@ -19,7 +19,7 @@
android:textStyle="bold"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Bitcoin" />
tools:text="Binance Smart Chain Optimal" />
<TextView
android:id="@+id/tv_status_verified"
@ -94,7 +94,7 @@
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:gravity="end"
android:gravity="end|bottom"
android:maxLines="1"
android:textColor="@color/darkGray6"
android:textSize="20sp"

View file

@ -45,10 +45,10 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:gravity="end"
android:layout_marginTop="4dp"
tools:text="@string/send_total_subtitle_format"
android:textColor="@color/darkGray1" />
android:gravity="end"
android:textColor="@color/darkGray1"
tools:text="123.29837729 ADA 39487593.109342039402938049 will be sent" />
</LinearLayout>

View file

@ -140,7 +140,7 @@
<!-- </com.google.android.material.chip.ChipGroup>-->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilMemo"
android:id="@+id/tilXlmMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
@ -148,7 +148,7 @@
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etMemo"
android:id="@+id/etXlmMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
@ -163,6 +163,38 @@
</LinearLayout>
<FrameLayout
android:id="@+id/binanceMemoContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/tilBinanceMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/send_extras_hint_memo"
app:boxBackgroundColor="@color/backgroundLightGray"
app:errorIconDrawable="@null">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/etBinanceMemo"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/backgroundLightGray"
android:ellipsize="end"
android:inputType="number"
android:paddingStart="0dp"
android:paddingEnd="0dp"
android:singleLine="true"
android:textSize="16sp"
tools:text="123" />
</com.google.android.material.textfield.TextInputLayout>
</FrameLayout>
<FrameLayout
android:id="@+id/xrpDestinationTagContainer"
android:layout_width="match_parent"

View file

@ -53,6 +53,7 @@
android:layout_height="82dp"
android:background="@color/backgroundLightGray"
android:fontFamily="sans-serif-light"
android:imeOptions="actionDone"
android:inputType="numberDecimal"
android:paddingStart="0dp"
android:paddingEnd="96dp"

View file

@ -179,6 +179,20 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/btn_copy" />
<TextView
android:id="@+id/tv_recieve_message"
style="@style/TextViewOnboarding.Body"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:textAlignment="center"
android:textAllCaps="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/iv_qr_code"
tools:text="@string/address_qr_code_message_token_format" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -9,6 +9,7 @@
android:id="@+id/btn_scan_short"
style="@style/TapBlackButton"
android:layout_width="0dp"
android:layout_height="52dp"
android:layout_marginStart="16dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="33dp"
@ -17,14 +18,15 @@
android:paddingTop="7dp"
android:text="@string/wallet_button_scan"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_top_up"
app:layout_constraintEnd_toStartOf="@id/btn_trade"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_top_up"
android:id="@+id/btn_trade"
style="@style/TapButton"
android:layout_width="0dp"
android:layout_height="52dp"
android:layout_marginStart="8dp"
android:layout_marginTop="30dp"
android:layout_marginBottom="33dp"
@ -42,6 +44,7 @@
android:id="@+id/btn_confirm_short"
style="@style/TapButton"
android:layout_width="0dp"
android:layout_height="52dp"
android:layout_marginStart="8dp"
android:layout_marginTop="30dp"
android:layout_marginEnd="16dp"
@ -52,7 +55,7 @@
android:text="@string/wallet_button_send"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/btn_top_up"
app:layout_constraintStart_toEndOf="@+id/btn_trade"
app:layout_constraintVertical_bias="1" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -25,7 +25,7 @@
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_card_background"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_height="230dp"
android:layout_marginStart="32dp"
android:layout_marginTop="67dp"
android:layout_marginEnd="32dp"

View file

@ -25,7 +25,7 @@
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/imv_card_background"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_height="230dp"
android:layout_marginStart="32dp"
android:layout_marginTop="@dimen/onboarding_square_background_margin_top"
android:layout_marginEnd="32dp"

View file

@ -9,6 +9,7 @@
android:layout_width="670dp"
android:layout_height="670dp"
android:src="@drawable/shape_circle_home_screen"
android:transitionName="bg_circle_large"
android:translationX="160dp"
android:translationY="-175dp" />
@ -18,14 +19,16 @@
android:layout_height="650dp"
android:src="@drawable/shape_circle"
android:tint="@color/backgroundWhite"
android:transitionName="bg_circle_medium"
android:translationX="170dp"
android:translationY="-165dp" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/bg_circle_small"
android:id="@+id/bg_circle_min"
android:layout_width="590dp"
android:layout_height="590dp"
android:src="@drawable/shape_circle_home_screen"
android:transitionName="bg_circle_min"
android:translationX="192dp"
android:translationY="-132dp" />

View file

@ -12,6 +12,7 @@
android:scaleType="fitXY"
android:src="@drawable/shape_circle"
android:tint="#DEDEE0"
android:transitionName="bg_circle_large"
android:translationX="-272dp"
android:translationY="-200dp"
app:layout_constraintStart_toStartOf="parent"
@ -25,6 +26,7 @@
android:scaleType="fitXY"
android:src="@drawable/shape_circle"
android:tint="#DCDCDC"
android:transitionName="bg_circle_medium"
android:translationX="-254dp"
android:translationY="-269dp"
app:layout_constraintStart_toStartOf="parent"
@ -38,6 +40,7 @@
android:scaleType="fitXY"
android:src="@drawable/shape_circle"
android:tint="#D9D9D9"
android:transitionName="bg_circle_min"
android:translationX="-394dp"
android:translationY="-411dp"
app:layout_constraintStart_toStartOf="parent"

View file

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingStart="16dp"
android:paddingEnd="16dp">
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_balance"
@ -18,19 +15,40 @@
android:text="@string/onboarding_balance_title"
android:textAllCaps="true"
android:textColor="#ABABAB"
android:textSize="14sp" />
<View
android:layout_width="wrap_content"
android:layout_height="4dp" />
android:textSize="14sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/tv_balance_value"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:letterSpacing="0.036"
android:maxLength="12"
android:maxLines="1"
android:textSize="28sp"
android:textStyle="bold"
app:layout_constraintEnd_toStartOf="@+id/tv_balance_currency"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_balance"
tools:text="01234890.0126789" />
<TextView
android:id="@+id/tv_balance_currency"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:letterSpacing="0.036"
android:textSize="28sp"
android:textStyle="bold"
tools:text="0.000 BTC" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toEndOf="@+id/tv_balance_value"
app:layout_constraintTop_toBottomOf="@id/tv_balance"
tools:text="BTC" />
</androidx.appcompat.widget.LinearLayoutCompat>
</androidx.constraintlayout.widget.ConstraintLayout>

Some files were not shown because too many files have changed in this diff Show more